From 38e4b1829bb9c4850e736f3680b72ec6036c2573 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Mar 2022 00:30:36 +0100 Subject: [PATCH 001/195] Build(deps): Bump rubocop from 1.26.0 to 1.26.1 (#16258) Bumps [rubocop](https://github.com/rubocop/rubocop) from 1.26.0 to 1.26.1. - [Release notes](https://github.com/rubocop/rubocop/releases) - [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop/rubocop/compare/v1.26.0...v1.26.1) --- updated-dependencies: - dependency-name: rubocop dependency-type: indirect update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7461e65e7d..34b9167e2f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -393,7 +393,7 @@ GEM json-schema (~> 2.2) railties (>= 3.1, < 7.1) rtlit (0.0.5) - rubocop (1.26.0) + rubocop (1.26.1) parallel (~> 1.10) parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) From cd7ce52138bed391d5efc56366e7a6517a6079e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Mar 2022 00:31:48 +0100 Subject: [PATCH 002/195] Build(deps): Bump concurrent-ruby from 1.1.9 to 1.1.10 (#16259) Bumps [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) from 1.1.9 to 1.1.10. - [Release notes](https://github.com/ruby-concurrency/concurrent-ruby/releases) - [Changelog](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/CHANGELOG.md) - [Commits](https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.1.9...v1.1.10) --- updated-dependencies: - dependency-name: concurrent-ruby dependency-type: indirect update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 34b9167e2f..8eb6a18971 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,7 +92,7 @@ GEM chunky_png (1.4.0) coderay (1.1.3) colored2 (3.1.2) - concurrent-ruby (1.1.9) + concurrent-ruby (1.1.10) connection_pool (2.2.5) cose (1.2.0) cbor (~> 0.5.9) From 8040b95e8c4abeab1ab9fe50826f92c52995a814 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Wed, 23 Mar 2022 12:43:08 +1000 Subject: [PATCH 003/195] DEV: Re-add polymorphic bookmark columns (#16261) This commit is a redo of2f1ddadff7dd47f824070c8a3f633f00a27aacde which we reverted because it blew up an internal CI check. I looked into it, and it happened because the old migration to add the bookmark columns still existed, and those columns were dropped in a post migrate, so the two migrations to add the columns were conflicting before the post migrate was run. ------ This commit only includes the creation of the new columns and index, and does not add any triggers, backfilling, or new data. A backfill will be done in the final PR when we switch this over. Intermediate PRs will look something like this: Add an experimental site setting for using polymorphic bookmarks, and make sure in the places where bookmarks are created or updated we fill in the columns. This setting will be used in subsequent PRs as well. Listing and searching bookmarks based on polymorphic associations Creating post and topic bookmarks using polymorphic associations, and changing special for_topic logic to just rely on the Topic bookmarkable_type Querying bookmark reminders based on polymorphic associations Make sure various other areas like importers, bookmark guardian, and others all rely on the associations Prepare plugins that rely on the Bookmark model to use polymorphic associations The final core PR will remove all the setting gates and switch over to using the polymorphic associations, backfill the bookmarks table columns, and ignore the old post_id and for_topic colummns. Then it will just be a matter of dropping the old columns down the line. --- app/models/bookmark.rb | 5 +++-- ...7011124_drop_bookmark_polymorphic_trigger.rb | 11 ----------- ...2024216_add_bookmark_polymorphic_columns.rb} | 0 ...7014925_drop_bookmark_polymorphic_columns.rb | 17 ----------------- 4 files changed, 3 insertions(+), 30 deletions(-) delete mode 100644 db/migrate/20220107011124_drop_bookmark_polymorphic_trigger.rb rename db/migrate/{20220104053343_add_bookmark_polymorphic_columns.rb => 20220322024216_add_bookmark_polymorphic_columns.rb} (100%) delete mode 100644 db/post_migrate/20220107014925_drop_bookmark_polymorphic_columns.rb diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb index e2f413bf13..35363b60d7 100644 --- a/app/models/bookmark.rb +++ b/app/models/bookmark.rb @@ -4,8 +4,6 @@ class Bookmark < ActiveRecord::Base # these columns were here for a very short amount of time, # hence the very short ignore time self.ignored_columns = [ - "bookmarkable_id", # TODO 2022-04-01 remove - "bookmarkable_type", # TODO 2022-04-01 remove "topic_id", # TODO 2022-04-01: remove "reminder_type" # TODO 2021-04-01: remove ] @@ -181,9 +179,12 @@ end # auto_delete_preference :integer default(0), not null # pinned :boolean default(FALSE) # for_topic :boolean default(FALSE), not null +# bookmarkable_id :integer +# bookmarkable_type :string # # Indexes # +# idx_bookmarks_user_polymorphic_unique (user_id,bookmarkable_type,bookmarkable_id) UNIQUE # index_bookmarks_on_post_id (post_id) # index_bookmarks_on_reminder_at (reminder_at) # index_bookmarks_on_reminder_set_at (reminder_set_at) diff --git a/db/migrate/20220107011124_drop_bookmark_polymorphic_trigger.rb b/db/migrate/20220107011124_drop_bookmark_polymorphic_trigger.rb deleted file mode 100644 index daacc978ca..0000000000 --- a/db/migrate/20220107011124_drop_bookmark_polymorphic_trigger.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class DropBookmarkPolymorphicTrigger < ActiveRecord::Migration[6.1] - def up - DB.exec("DROP FUNCTION IF EXISTS sync_bookmarks_polymorphic_column_data CASCADE") - end - - def down - raise ActiveRecord::IrreversibleMigration - end -end diff --git a/db/migrate/20220104053343_add_bookmark_polymorphic_columns.rb b/db/migrate/20220322024216_add_bookmark_polymorphic_columns.rb similarity index 100% rename from db/migrate/20220104053343_add_bookmark_polymorphic_columns.rb rename to db/migrate/20220322024216_add_bookmark_polymorphic_columns.rb diff --git a/db/post_migrate/20220107014925_drop_bookmark_polymorphic_columns.rb b/db/post_migrate/20220107014925_drop_bookmark_polymorphic_columns.rb deleted file mode 100644 index 0fbe6cb762..0000000000 --- a/db/post_migrate/20220107014925_drop_bookmark_polymorphic_columns.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class DropBookmarkPolymorphicColumns < ActiveRecord::Migration[6.1] - DROPPED_COLUMNS ||= { - bookmarks: %i{bookmarkable_id bookmarkable_type} - } - - def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end - end - - def down - raise ActiveRecord::IrreversibleMigration - end -end From eb237e634a440f4b26e6c097e7afb9a135bb3781 Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Wed, 23 Mar 2022 13:03:56 +0300 Subject: [PATCH 004/195] A11Y: Focus last viewed topic in topic lists (take 3) (#16257) Another attempt at fixing https://meta.discourse.org/t/discourse-with-a-screen-reader/178105/88?u=osama. Previous PR (reverted): #16240. The problems with the previous PR were: 1. As you scrolled down a topics list, the first topic of every new batch of topics would receive focus and the indicator would show up. 2. Similar to 1, clicking the `See X new or updated topics` notice would also focus a random topic from the new topics that were just loaded. 3. Topics in the suggested topics list received focus too 4. Our custom focus indicator appeared on mobile, but it shouldn't. This commit should have none of these problems. --- .../app/components/topic-list-item.js | 52 ++++++++++++++++--- .../app/templates/components/topic-list.hbs | 3 +- .../app/templates/discovery/topics.hbs | 4 +- .../discourse/app/templates/tag/show.hbs | 1 + .../last-visited-topic-focus-test.js | 26 ++++++++++ .../stylesheets/common/base/_topic-list.scss | 11 ++++ 6 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/discourse/tests/acceptance/last-visited-topic-focus-test.js diff --git a/app/assets/javascripts/discourse/app/components/topic-list-item.js b/app/assets/javascripts/discourse/app/components/topic-list-item.js index 7e3a46dec8..a1cec9d310 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -1,4 +1,7 @@ -import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import discourseComputed, { + bind, + observes, +} from "discourse-common/utils/decorators"; import Component from "@ember/component"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; @@ -58,6 +61,11 @@ export default Component.extend({ if (this.selected && this.selected.includes(this.topic)) { this.element.querySelector("input.bulk-select").checked = true; } + const title = this.element.querySelector(".main-link .title"); + if (title) { + title.addEventListener("focus", this._onTitleFocus); + title.addEventListener("blur", this._onTitleBlur); + } }); } }, @@ -98,6 +106,11 @@ export default Component.extend({ if (this.includeUnreadIndicator) { this.messageBus.unsubscribe(this.unreadIndicatorChannel); } + const title = this.element?.querySelector(".main-link .title"); + if (title) { + title.removeEventListener("focus", this._onTitleFocus); + title.removeEventListener("blur", this._onTitleBlur); + } }, @discourseComputed("topic.id") @@ -259,12 +272,21 @@ export default Component.extend({ return; } - const $topic = $(this.element); - $topic - .addClass("highlighted") - .attr("data-islastviewedtopic", opts.isLastViewedTopic); - - $topic.on("animationend", () => $topic.removeClass("highlighted")); + this.element.classList.add("highlighted"); + this.element.setAttribute( + "data-islastviewedtopic", + opts.isLastViewedTopic + ); + this.element.addEventListener("animationend", () => { + this.element.classList.remove("highlighted"); + }); + if ( + this.focusLastVisitedTopic && + opts.isLastViewedTopic && + !this.site.mobileView + ) { + this.element.querySelector(".main-link .title").focus(); + } }); }, @@ -279,4 +301,20 @@ export default Component.extend({ this.highlight(); } }), + + @bind + _onTitleFocus() { + if (this.element && !this.isDestroying && !this.isDestroyed) { + const mainLink = this.element.querySelector(".main-link"); + mainLink.classList.add("focused"); + } + }, + + @bind + _onTitleBlur() { + if (this.element && !this.isDestroying && !this.isDestroyed) { + const mainLink = this.element.querySelector(".main-link"); + mainLink.classList.remove("focused"); + } + }, }); diff --git a/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs b/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs index 3ff3f75731..3a554aea95 100644 --- a/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs @@ -42,7 +42,8 @@ lastVisitedTopic=lastVisitedTopic selected=selected lastChecked=lastChecked - tagsForUser=tagsForUser}} + tagsForUser=tagsForUser + focusLastVisitedTopic=focusLastVisitedTopic}} {{raw "list/visited-line" lastVisitedTopic=lastVisitedTopic topic=topic}} {{/each}} diff --git a/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs index 3c2997c58c..946d599382 100644 --- a/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs @@ -62,7 +62,9 @@ model=model showResetNew=showResetNew showDismissRead=showDismissRead resetNew=( topics=model.topics discoveryList=true scrollOnLoad=true - onScroll=discoveryTopicList.saveScrollPosition}} + onScroll=discoveryTopicList.saveScrollPosition + focusLastVisitedTopic=true + }} {{/if}} {{plugin-outlet name="after-topic-list" tagName="span" connectorTagName="div" args=(hash category=category)}} diff --git a/app/assets/javascripts/discourse/app/templates/tag/show.hbs b/app/assets/javascripts/discourse/app/templates/tag/show.hbs index 917209dd74..49dedfdf66 100644 --- a/app/assets/javascripts/discourse/app/templates/tag/show.hbs +++ b/app/assets/javascripts/discourse/app/templates/tag/show.hbs @@ -88,6 +88,7 @@ changeSort=(action "changeSort") onScroll=discoveryTopicList.saveScrollPosition scrollOnLoad=true + focusLastVisitedTopic=true }} {{/if}} {{/discovery-topics-list}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/last-visited-topic-focus-test.js b/app/assets/javascripts/discourse/tests/acceptance/last-visited-topic-focus-test.js new file mode 100644 index 0000000000..b2456c72da --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/last-visited-topic-focus-test.js @@ -0,0 +1,26 @@ +import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; +import { test } from "qunit"; +import { visit } from "@ember/test-helpers"; +import { cloneJSON } from "discourse-common/lib/object"; +import topicFixtures from "discourse/tests/fixtures/topic"; + +acceptance("Last Visited Topic Focus", function (needs) { + needs.pretender((server, helper) => { + const fixture = cloneJSON(topicFixtures["/t/54077.json"]); + fixture.id = 11996; + fixture.slug = + "its-really-hard-to-navigate-the-create-topic-reply-pane-with-the-keyboard"; + server.get("/t/11996.json", () => helper.response(fixture)); + }); + test("last visited topic receives focus when you return back to the topic list", async function (assert) { + await visit("/"); + await visit( + "/t/its-really-hard-to-navigate-the-create-topic-reply-pane-with-the-keyboard/11996" + ); + await visit("/"); + const visitedTopicTitle = query( + '.topic-list-body tr[data-topic-id="11996"] .main-link' + ); + assert.ok(visitedTopicTitle.classList.contains("focused")); + }); +}); diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index ed62a5c702..90646ddcac 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -234,6 +234,17 @@ .raw-topic-link > * { pointer-events: none; } + + &.focused { + box-shadow: inset 3px 0 0 var(--tertiary); + } + /* we have a custom focus indicator so we can remove the native one */ + .title:focus { + outline: none; + } + .title:focus-visible { + outline: none; + } } .unread-indicator { From 97e7bb1ce483498640f512c1bf9bfe704dc1d402 Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Wed, 23 Mar 2022 15:30:11 +0300 Subject: [PATCH 005/195] FIX: Don't listen for focus/blur events if the topic-list opts out of last visited focus (#16263) Follow-up to https://github.com/discourse/discourse/commit/eb237e634a440f4b26e6c097e7afb9a135bb3781. Some `{{topic-list}}` instances, like the one for suggested topics, opt out of focusing the row of the last visited topic in the list, but we currently still add listeners for focus/blur events even if when the topic-list instance opts out. This commit adds a check so that we only register focus/blur listeners if the topic-list opts in for last visited topic focus. --- .../app/components/topic-list-item.js | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/topic-list-item.js b/app/assets/javascripts/discourse/app/components/topic-list-item.js index a1cec9d310..806da566fd 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -61,10 +61,12 @@ export default Component.extend({ if (this.selected && this.selected.includes(this.topic)) { this.element.querySelector("input.bulk-select").checked = true; } - const title = this.element.querySelector(".main-link .title"); - if (title) { - title.addEventListener("focus", this._onTitleFocus); - title.addEventListener("blur", this._onTitleBlur); + if (this._shouldFocusLastVisited()) { + const title = this._titleElement(); + if (title) { + title.addEventListener("focus", this._onTitleFocus); + title.addEventListener("blur", this._onTitleBlur); + } } }); } @@ -106,10 +108,12 @@ export default Component.extend({ if (this.includeUnreadIndicator) { this.messageBus.unsubscribe(this.unreadIndicatorChannel); } - const title = this.element?.querySelector(".main-link .title"); - if (title) { - title.removeEventListener("focus", this._onTitleFocus); - title.removeEventListener("blur", this._onTitleBlur); + if (this._shouldFocusLastVisited()) { + const title = this._titleElement(); + if (title) { + title.removeEventListener("focus", this._onTitleFocus); + title.removeEventListener("blur", this._onTitleBlur); + } } }, @@ -280,12 +284,8 @@ export default Component.extend({ this.element.addEventListener("animationend", () => { this.element.classList.remove("highlighted"); }); - if ( - this.focusLastVisitedTopic && - opts.isLastViewedTopic && - !this.site.mobileView - ) { - this.element.querySelector(".main-link .title").focus(); + if (opts.isLastViewedTopic && this._shouldFocusLastVisited()) { + this._titleElement().focus(); } }); }, @@ -305,16 +305,26 @@ export default Component.extend({ @bind _onTitleFocus() { if (this.element && !this.isDestroying && !this.isDestroyed) { - const mainLink = this.element.querySelector(".main-link"); - mainLink.classList.add("focused"); + this._mainLinkElement().classList.add("focused"); } }, @bind _onTitleBlur() { if (this.element && !this.isDestroying && !this.isDestroyed) { - const mainLink = this.element.querySelector(".main-link"); - mainLink.classList.remove("focused"); + this._mainLinkElement().classList.remove("focused"); } }, + + _shouldFocusLastVisited() { + return !this.site.mobileView && this.focusLastVisitedTopic; + }, + + _mainLinkElement() { + return this.element.querySelector(".main-link"); + }, + + _titleElement() { + return this.element.querySelector(".main-link .title"); + }, }); From 7fcf4dcd4bf7281c1f373c09ac93c384f58e0e62 Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Wed, 23 Mar 2022 14:28:09 +0100 Subject: [PATCH 006/195] FIX: Allow `@ember/test` import in embercli prod builds (#16264) This matches the behavior of legacy discourse-loader and the regular Ember resolver. --- app/assets/javascripts/discourse/lib/rfc176-shims/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/rfc176-shims/index.js b/app/assets/javascripts/discourse/lib/rfc176-shims/index.js index df26adac62..cb0bb9460c 100644 --- a/app/assets/javascripts/discourse/lib/rfc176-shims/index.js +++ b/app/assets/javascripts/discourse/lib/rfc176-shims/index.js @@ -40,7 +40,11 @@ module.exports = { m = modules[entry.module] = []; } - m.push(entry); + if (entry.module === "@ember/test") { + m.push({ ...entry, global: `(Ember.Test && ${entry.global})` }); + } else { + m.push(entry); + } } let output = ""; From 147ffadcf3bb4bdc393bfec3db64ad2bea766095 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Wed, 23 Mar 2022 09:28:55 -0400 Subject: [PATCH 007/195] DEV: Update Uppy to 2.1.6 (#16227) --- .../javascripts/discourse-common/package.json | 12 +- app/assets/javascripts/discourse/package.json | 12 +- app/assets/javascripts/yarn.lock | 86 +- package.json | 12 +- vendor/assets/javascripts/uppy.js | 1597 +++++++++-------- yarn.lock | 86 +- 6 files changed, 917 insertions(+), 888 deletions(-) diff --git a/app/assets/javascripts/discourse-common/package.json b/app/assets/javascripts/discourse-common/package.json index 68f1a4c629..ebf77fa1e3 100644 --- a/app/assets/javascripts/discourse-common/package.json +++ b/app/assets/javascripts/discourse-common/package.json @@ -15,12 +15,12 @@ "start": "ember serve" }, "dependencies": { - "@uppy/aws-s3": "^2.0.4", - "@uppy/aws-s3-multipart": "^2.1.0", - "@uppy/core": "^2.1.0", - "@uppy/drop-target": "^1.1.0", - "@uppy/utils": "^4.0.3", - "@uppy/xhr-upload": "^2.0.4", + "@uppy/aws-s3": "^2.0.8", + "@uppy/aws-s3-multipart": "^2.2.1", + "@uppy/core": "^2.1.6", + "@uppy/drop-target": "^1.1.2", + "@uppy/utils": "^4.0.5", + "@uppy/xhr-upload": "^2.0.7", "ember-auto-import": "^2.2.4", "ember-cli-babel": "^7.13.0", "ember-cli-htmlbars": "^4.2.0", diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 95f27709c9..f4896e1c3a 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -24,12 +24,12 @@ "@glimmer/component": "^1.0.4", "@glimmer/tracking": "^1.0.4", "@popperjs/core": "2.10.2", - "@uppy/aws-s3": "^2.0.4", - "@uppy/aws-s3-multipart": "^2.1.0", - "@uppy/core": "^2.1.0", - "@uppy/drop-target": "^1.1.0", - "@uppy/utils": "^4.0.3", - "@uppy/xhr-upload": "^2.0.4", + "@uppy/aws-s3": "^2.0.8", + "@uppy/aws-s3-multipart": "^2.2.1", + "@uppy/core": "^2.1.6", + "@uppy/drop-target": "^1.1.2", + "@uppy/utils": "^4.0.5", + "@uppy/xhr-upload": "^2.0.7", "admin": "^1.0.0", "broccoli-asset-rev": "^3.0.0", "deepmerge": "^4.2.2", diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index cc6228b5bd..33e0110832 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -2318,72 +2318,72 @@ resolved "https://registry.yarnpkg.com/@types/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz#4151a81b4052c80bc2becbae09f3a9ec010a9c7a" integrity sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg== -"@uppy/aws-s3-multipart@^2.1.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@uppy/aws-s3-multipart/-/aws-s3-multipart-2.1.1.tgz#7749491067ab72249dab201cc12409e57f2dbb1a" - integrity sha512-p+oFSCWEUc7ptv73sdZuWoq10hh0vzmP4cxwBEX/+nrplLFSuRUJ+z2XnNEigo8jXHWbA86k6tEX/3XIUsslgg== +"@uppy/aws-s3-multipart@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@uppy/aws-s3-multipart/-/aws-s3-multipart-2.2.1.tgz#385705b3ae6b56abee28fb086c046eeaeceee62a" + integrity sha512-57MZw2hxcBVeXp7xxdPNna+7HMckiJsrq/vwNM74aforLpNNSYE1B3JsiBeXU7fvIWx/W+5udtl8aAIKQxpJqw== dependencies: - "@uppy/companion-client" "^2.0.3" - "@uppy/utils" "^4.0.3" + "@uppy/companion-client" "^2.0.5" + "@uppy/utils" "^4.0.5" -"@uppy/aws-s3@^2.0.4": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@uppy/aws-s3/-/aws-s3-2.0.5.tgz#dae2edb819b8e79119304a1659b931a862bf1e45" - integrity sha512-VWqVtmKtV/wSLCZdFbWlUt+CS7W/KZv20Pmm3JgcDLrQk3PdciYg3L9x65FTP8kSDsiXCwMg7uO5HfbspZWx9Q== +"@uppy/aws-s3@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@uppy/aws-s3/-/aws-s3-2.0.8.tgz#fd82b7db4d54ed118d369f856efe3d849e0482e3" + integrity sha512-ihZF3SpXZCZPxNapXshBhSC4TwNv0JlASZqd6T+u48Ojb6FZbYs7BgXLnLQooOGZPUx1UXtJREBh9SjXvn1lWw== dependencies: - "@uppy/companion-client" "^2.0.3" - "@uppy/utils" "^4.0.3" - "@uppy/xhr-upload" "^2.0.5" + "@uppy/companion-client" "^2.0.5" + "@uppy/utils" "^4.0.5" + "@uppy/xhr-upload" "^2.0.7" nanoid "^3.1.25" -"@uppy/companion-client@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-2.0.3.tgz#d3cd30ebbc9f87d27374d13258b5d304366f10d5" - integrity sha512-I1baKKBpb3d//q3agRtNV3UD/sA7EecFOfoVSpMlPkFu6oQqxjSC5OFXTf3fa8X+wo4Lcutv1++3igPJ1zrgbA== +"@uppy/companion-client@^2.0.4", "@uppy/companion-client@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-2.0.5.tgz#ee0c177dac22afab132369f467d82e7017eeb468" + integrity sha512-yAeYbpQ+yHcklKVbkRy83V1Zv/0kvaTDTHaBvaaPmLtcKgeZE3pUjEI/7v2sTxvCVSy4cRjd9TRSXSSl5UCnuQ== dependencies: - "@uppy/utils" "^4.0.3" + "@uppy/utils" "^4.0.5" namespace-emitter "^2.0.1" -"@uppy/core@^2.1.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@uppy/core/-/core-2.1.1.tgz#503b3172ffe32e6cc7385f5b0c99f008ade815f1" - integrity sha512-dFlcy6+05zwsJk1KNeUKVWUyAfhOVwNpnPLaR1NX9Qsjv7KlYfUNRVW3uCCmIpd/EZsX44+haiqGrhLcYDAcxA== +"@uppy/core@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@uppy/core/-/core-2.1.6.tgz#8e3c6eca12c91118a6340a1aedc777c6b4f2f6f5" + integrity sha512-WTGthAAHMfB6uAtISbu+7jYh4opnBWHSf7A0jsPdREwXc4hrhC/z9lbejZfSLkVDXdbNwpWWH38EgOGCNQb5MQ== dependencies: "@transloadit/prettier-bytes" "0.0.7" - "@uppy/store-default" "^2.0.2" - "@uppy/utils" "^4.0.3" + "@uppy/store-default" "^2.0.3" + "@uppy/utils" "^4.0.5" lodash.throttle "^4.1.1" mime-match "^1.0.2" namespace-emitter "^2.0.1" nanoid "^3.1.25" preact "^10.5.13" -"@uppy/drop-target@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@uppy/drop-target/-/drop-target-1.1.1.tgz#9bfbcb7b284ef605d01fc24823f857cbad51377a" - integrity sha512-2MxNGEkI2vt1D6MEa0PNqR+VTMbuUzmiytHyy57phZNCNes8K4BdnneBwla2nG3LI0D1TURK7MKxaSjv93d3Vg== +"@uppy/drop-target@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@uppy/drop-target/-/drop-target-1.1.2.tgz#a78cc1c1947e55be4e16de71efdbacf9dfb7effd" + integrity sha512-iyLckwpxDqZr7ysH94cWwgta9P9SFus0sayXb9Lr/Kd0lk+tK/bcrJmsJHp4HYDFW7C8RphYdUu78C8NNXL09w== dependencies: - "@uppy/utils" "^4.0.3" + "@uppy/utils" "^4.0.5" -"@uppy/store-default@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-2.0.2.tgz#c0464e92452fdc7d4cd1548d2c7453017cad7a98" - integrity sha512-D9oz08EYBoc4fDotvaevd2Q7uVldS61HYFOXK20b5M/xXF/uxepapaqQnMu1DfCVsA77rhp7DMemxnWc9y8xTQ== +"@uppy/store-default@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-2.0.3.tgz#47ad4fc4816d21955ff37d6bb5096a93278c29e2" + integrity sha512-2BGlN1sW0cFv4rOqTK8dfSg579S984N1HxCJxLFqeW9nWD6zd/O8Omyd85tbxGQ+FLZLTmLOm/feD0YeCBMahg== -"@uppy/utils@^4.0.3": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-4.0.3.tgz#181fdd161e1450d31af0cf7bc97946a99196a8fe" - integrity sha512-LApneC8lNvTonzSJFupxzuEvKhwp/Klc1otq8t+zXpdgjLVVSuW/rJBFfdIDrmDoqSzVLQKYjMy07CmhDAWfKg== +"@uppy/utils@^4.0.4", "@uppy/utils@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-4.0.5.tgz#0feda6e03d13af2fec969b146d7410462a8d2d48" + integrity sha512-uRv921A69UMjuWCLSC5tKXuIVoMOROVpFstIAQv5CoiCOCXyofcWpvAqELT7qlQJ5VRWha3uF5d/Z94SNnwxew== dependencies: lodash.throttle "^4.1.1" -"@uppy/xhr-upload@^2.0.4", "@uppy/xhr-upload@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@uppy/xhr-upload/-/xhr-upload-2.0.5.tgz#5792a7ff0bfb1503c8a9cccefb48ddb40deb11de" - integrity sha512-DkD6cRKrcI4oDmCimHAULb6rruyUt6SbH4/omhpvWILbG/mWV5vA39YLvYxCZ1FZbijJ4QkVTKEeOTLcmoljPg== +"@uppy/xhr-upload@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@uppy/xhr-upload/-/xhr-upload-2.0.7.tgz#d9128be1fdde78edc61878e23930a2855f607df5" + integrity sha512-bzCc654B0HfNmL4BIr7gGTvg2pQBucYgPmAb4ST7jGyWlEJWbSxMXR/19zvISQzpJ6v1uP6q2ppgxGMqNdj/rA== dependencies: - "@uppy/companion-client" "^2.0.3" - "@uppy/utils" "^4.0.3" + "@uppy/companion-client" "^2.0.4" + "@uppy/utils" "^4.0.4" nanoid "^3.1.25" "@webassemblyjs/ast@1.11.1": diff --git a/package.json b/package.json index 7507947a57..a365c0085f 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,12 @@ "@highlightjs/cdn-assets": "^10.7.0", "@json-editor/json-editor": "^2.6.1", "@popperjs/core": "v2.10.2", - "@uppy/aws-s3": "^2.0.4", - "@uppy/aws-s3-multipart": "^2.1.0", - "@uppy/core": "^2.1.0", - "@uppy/drop-target": "^1.1.0", - "@uppy/utils": "^4.0.3", - "@uppy/xhr-upload": "^2.0.4", + "@uppy/aws-s3": "^2.0.8", + "@uppy/aws-s3-multipart": "^2.2.1", + "@uppy/core": "^2.1.6", + "@uppy/drop-target": "^1.1.2", + "@uppy/utils": "^4.0.5", + "@uppy/xhr-upload": "^2.0.7", "ace-builds": "1.4.13", "bootbox": "3.2.0", "bootstrap": "v3.4.1", diff --git a/vendor/assets/javascripts/uppy.js b/vendor/assets/javascripts/uppy.js index 32bfb01026..0c526f8a8c 100644 --- a/vendor/assets/javascripts/uppy.js +++ b/vendor/assets/javascripts/uppy.js @@ -32,8 +32,6 @@ module.exports = function prettierBytes (num) { } },{}],2:[function(require,module,exports){ -"use strict"; - function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } var id = 0; @@ -185,7 +183,6 @@ class MultipartUploader { this.partsInProgress = 0; this.chunks = null; this.chunkState = null; - this.lockedCandidatesForBatch = []; _classPrivateFieldLooseBase(this, _initChunks)[_initChunks](); @@ -217,8 +214,14 @@ class MultipartUploader { this.isPaused = true; } - abort(opts = undefined) { - if (opts != null && opts.really) _classPrivateFieldLooseBase(this, _abortUpload)[_abortUpload]();else this.pause(); + abort(opts) { + var _opts; + + if (opts === void 0) { + opts = undefined; + } + + if ((_opts = opts) != null && _opts.really) _classPrivateFieldLooseBase(this, _abortUpload)[_abortUpload]();else this.pause(); } } @@ -329,8 +332,6 @@ function _uploadParts2() { const candidates = []; for (let i = 0; i < this.chunkState.length; i++) { - // eslint-disable-next-line no-continue - if (this.lockedCandidatesForBatch.includes(i)) continue; const state = this.chunkState[i]; // eslint-disable-next-line no-continue if (state.done || state.busy) continue; @@ -360,11 +361,12 @@ function _uploadParts2() { }); } -function _retryable2({ - before, - attempt, - after -}) { +function _retryable2(_ref) { + let { + before, + attempt, + after + } = _ref; const { retryDelays } = this.options; @@ -407,12 +409,18 @@ function _retryable2({ } async function _prepareUploadParts2(candidates) { - this.lockedCandidatesForBatch.push(...candidates); + candidates.forEach(i => { + this.chunkState[i].busy = true; + }); const result = await _classPrivateFieldLooseBase(this, _retryable)[_retryable]({ attempt: () => this.options.prepareUploadParts({ key: this.key, uploadId: this.uploadId, - partNumbers: candidates.map(index => index + 1) + partNumbers: candidates.map(index => index + 1), + chunks: candidates.reduce((chunks, candidate) => ({ ...chunks, + // Use the part number as the index + [candidate + 1]: this.chunks[candidate] + }), {}) }) }); @@ -591,9 +599,7 @@ function _onError2(err) { } module.exports = MultipartUploader; -},{"@uppy/utils/lib/AbortController":23,"@uppy/utils/lib/delay":29}],3:[function(require,module,exports){ -"use strict"; - +},{"@uppy/utils/lib/AbortController":26,"@uppy/utils/lib/delay":32}],3:[function(require,module,exports){ var _class, _temp; const BasePlugin = require('@uppy/core/lib/BasePlugin'); @@ -614,7 +620,7 @@ const { RateLimitedQueue } = require('@uppy/utils/lib/RateLimitedQueue'); -const Uploader = require('./MultipartUploader'); +const MultipartUploader = require('./MultipartUploader'); function assertServerError(res) { if (res && res.error) { @@ -661,7 +667,11 @@ module.exports = (_temp = _class = class AwsS3Multipart extends BasePlugin { */ - resetUploaderReferences(fileID, opts = {}) { + resetUploaderReferences(fileID, opts) { + if (opts === void 0) { + opts = {}; + } + if (this.uploaders[fileID]) { this.uploaders[fileID].abort({ really: opts.abort || false @@ -701,30 +711,33 @@ module.exports = (_temp = _class = class AwsS3Multipart extends BasePlugin { }).then(assertServerError); } - listParts(file, { - key, - uploadId - }) { + listParts(file, _ref) { + let { + key, + uploadId + } = _ref; this.assertHost('listParts'); const filename = encodeURIComponent(key); return this.client.get(`s3/multipart/${uploadId}?key=${filename}`).then(assertServerError); } - prepareUploadParts(file, { - key, - uploadId, - partNumbers - }) { + prepareUploadParts(file, _ref2) { + let { + key, + uploadId, + partNumbers + } = _ref2; this.assertHost('prepareUploadParts'); const filename = encodeURIComponent(key); return this.client.get(`s3/multipart/${uploadId}/batch?key=${filename}&partNumbers=${partNumbers.join(',')}`).then(assertServerError); } - completeMultipartUpload(file, { - key, - uploadId, - parts - }) { + completeMultipartUpload(file, _ref3) { + let { + key, + uploadId, + parts + } = _ref3; this.assertHost('completeMultipartUpload'); const filename = encodeURIComponent(key); const uploadIdEnc = encodeURIComponent(uploadId); @@ -733,10 +746,11 @@ module.exports = (_temp = _class = class AwsS3Multipart extends BasePlugin { }).then(assertServerError); } - abortMultipartUpload(file, { - key, - uploadId - }) { + abortMultipartUpload(file, _ref4) { + let { + key, + uploadId + } = _ref4; this.assertHost('abortMultipartUpload'); const filename = encodeURIComponent(key); const uploadIdEnc = encodeURIComponent(uploadId); @@ -799,7 +813,7 @@ module.exports = (_temp = _class = class AwsS3Multipart extends BasePlugin { this.uppy.emit('s3-multipart:part-uploaded', cFile, part); }; - const upload = new Uploader(file.data, { + const upload = new MultipartUploader(file.data, { // .bind to pass the file object to each handler. createMultipartUpload: this.opts.createMultipartUpload.bind(this, file), listParts: this.opts.listParts.bind(this, file), @@ -928,7 +942,7 @@ module.exports = (_temp = _class = class AwsS3Multipart extends BasePlugin { this.uploaderEvents[file.id] = new EventTracker(this.uppy); this.onFileRemove(file.id, () => { queuedRequest.abort(); - socket.send('pause', {}); + socket.send('cancel', {}); this.resetUploaderReferences(file.id, { abort: true }); @@ -955,7 +969,7 @@ module.exports = (_temp = _class = class AwsS3Multipart extends BasePlugin { }); this.onCancelAll(file.id, () => { queuedRequest.abort(); - socket.send('pause', {}); + socket.send('cancel', {}); this.resetUploaderReferences(file.id); resolve(`upload ${file.id} was canceled`); }); @@ -1103,10 +1117,8 @@ module.exports = (_temp = _class = class AwsS3Multipart extends BasePlugin { this.uppy.removeUploader(this.upload); } -}, _class.VERSION = "2.1.1", _temp); -},{"./MultipartUploader":2,"@uppy/companion-client":12,"@uppy/core/lib/BasePlugin":14,"@uppy/utils/lib/EventTracker":24,"@uppy/utils/lib/RateLimitedQueue":27,"@uppy/utils/lib/emitSocketProgress":30,"@uppy/utils/lib/getSocketHost":41}],4:[function(require,module,exports){ -"use strict"; - +}, _class.VERSION = "2.2.1", _temp); +},{"./MultipartUploader":2,"@uppy/companion-client":13,"@uppy/core/lib/BasePlugin":15,"@uppy/utils/lib/EventTracker":27,"@uppy/utils/lib/RateLimitedQueue":30,"@uppy/utils/lib/emitSocketProgress":33,"@uppy/utils/lib/getSocketHost":44}],4:[function(require,module,exports){ var _getOptions, _addEventHandlerForFile, _addEventHandlerIfFileStillExists, _uploadLocalFile, _uploadRemoteFile; function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } @@ -1117,7 +1129,7 @@ function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + n const { nanoid -} = require('nanoid'); +} = require('nanoid/non-secure'); const { Provider, @@ -1420,13 +1432,13 @@ function _uploadRemoteFile2(file) { }); _classPrivateFieldLooseBase(this, _addEventHandlerForFile)[_addEventHandlerForFile]('file-removed', file.id, () => { - socket.send('pause', {}); + socket.send('cancel', {}); queuedRequest.abort(); resolve(`upload ${file.id} was removed`); }); _classPrivateFieldLooseBase(this, _addEventHandlerIfFileStillExists)[_addEventHandlerIfFileStillExists]('cancel-all', file.id, () => { - socket.send('pause', {}); + socket.send('cancel', {}); queuedRequest.abort(); resolve(`upload ${file.id} was canceled`); }); @@ -1481,9 +1493,7 @@ function _uploadRemoteFile2(file) { return Promise.reject(err); })); } -},{"@uppy/companion-client":12,"@uppy/utils/lib/EventTracker":24,"@uppy/utils/lib/NetworkError":25,"@uppy/utils/lib/ProgressTimeout":26,"@uppy/utils/lib/RateLimitedQueue":27,"@uppy/utils/lib/emitSocketProgress":30,"@uppy/utils/lib/getSocketHost":41,"@uppy/utils/lib/isNetworkError":45,"nanoid":53}],5:[function(require,module,exports){ -"use strict"; - +},{"@uppy/companion-client":13,"@uppy/utils/lib/EventTracker":27,"@uppy/utils/lib/NetworkError":28,"@uppy/utils/lib/ProgressTimeout":29,"@uppy/utils/lib/RateLimitedQueue":30,"@uppy/utils/lib/emitSocketProgress":33,"@uppy/utils/lib/getSocketHost":44,"@uppy/utils/lib/isNetworkError":48,"nanoid/non-secure":57}],5:[function(require,module,exports){ var _class, _client, _requests, _uploader, _handleUpload, _temp; function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } @@ -1525,8 +1535,6 @@ const { internalRateLimitedQueue } = require('@uppy/utils/lib/RateLimitedQueue'); -const settle = require('@uppy/utils/lib/settle'); - const { RequestClient } = require('@uppy/companion-client'); @@ -1535,6 +1543,8 @@ const MiniXHRUpload = require('./MiniXHRUpload'); const isXml = require('./isXml'); +const locale = require('./locale'); + function resolveUrl(origin, link) { return new URL(link, origin || undefined).toString(); } @@ -1641,7 +1651,7 @@ module.exports = (_temp = (_client = /*#__PURE__*/_classPrivateFieldLooseKey("cl }); const numberOfFiles = fileIDs.length; - return settle(fileIDs.map((id, index) => { + return Promise.allSettled(fileIDs.map((id, index) => { paramsPromises[id] = getUploadParameters(this.uppy.getFile(id)); return paramsPromises[id].then(params => { delete paramsPromises[id]; @@ -1675,6 +1685,7 @@ module.exports = (_temp = (_client = /*#__PURE__*/_classPrivateFieldLooseKey("cl delete paramsPromises[id]; const file = this.uppy.getFile(id); this.uppy.emit('upload-error', file, error); + return Promise.reject(error); }); })).finally(() => { // cleanup. @@ -1685,11 +1696,7 @@ module.exports = (_temp = (_client = /*#__PURE__*/_classPrivateFieldLooseKey("cl this.type = 'uploader'; this.id = this.opts.id || 'AwsS3'; this.title = 'AWS S3'; - this.defaultLocale = { - strings: { - timedOut: 'Upload stalled for %{seconds} seconds, aborting.' - } - }; + this.defaultLocale = locale; const defaultOptions = { timeout: 30 * 1000, limit: 0, @@ -1795,10 +1802,8 @@ module.exports = (_temp = (_client = /*#__PURE__*/_classPrivateFieldLooseKey("cl this.uppy.removeUploader(_classPrivateFieldLooseBase(this, _handleUpload)[_handleUpload]); } -}), _class.VERSION = "2.0.5", _temp); -},{"./MiniXHRUpload":4,"./isXml":6,"@uppy/companion-client":12,"@uppy/core/lib/BasePlugin":14,"@uppy/utils/lib/RateLimitedQueue":27,"@uppy/utils/lib/settle":47}],6:[function(require,module,exports){ -"use strict"; - +}), _class.VERSION = "2.0.8", _temp); +},{"./MiniXHRUpload":4,"./isXml":6,"./locale":7,"@uppy/companion-client":13,"@uppy/core/lib/BasePlugin":15,"@uppy/utils/lib/RateLimitedQueue":30}],6:[function(require,module,exports){ /** * Remove parameters like `charset=utf-8` from the end of a mime type string. * @@ -1839,6 +1844,12 @@ function isXml(content, xhr) { module.exports = isXml; },{}],7:[function(require,module,exports){ +module.exports = { + strings: { + timedOut: 'Upload stalled for %{seconds} seconds, aborting.' + } +}; +},{}],8:[function(require,module,exports){ 'use strict'; class AuthError extends Error { @@ -1851,7 +1862,7 @@ class AuthError extends Error { } module.exports = AuthError; -},{}],8:[function(require,module,exports){ +},{}],9:[function(require,module,exports){ 'use strict'; const RequestClient = require('./RequestClient'); @@ -1875,7 +1886,8 @@ module.exports = class Provider extends RequestClient { } headers() { - return Promise.all([super.headers(), this.getAuthToken()]).then(([headers, token]) => { + return Promise.all([super.headers(), this.getAuthToken()]).then(_ref => { + let [headers, token] = _ref; const authHeaders = {}; if (token) { @@ -1912,31 +1924,53 @@ module.exports = class Provider extends RequestClient { getAuthToken() { return this.uppy.getPlugin(this.pluginId).storage.getItem(this.tokenKey); } + /** + * Ensure we have a preauth token if necessary. Attempts to fetch one if we don't, + * or rejects if loading one fails. + */ - authUrl(queries = {}) { - if (this.preAuthToken) { - queries.uppyPreAuthToken = this.preAuthToken; + + async ensurePreAuth() { + if (this.companionKeysParams && !this.preAuthToken) { + await this.fetchPreAuthToken(); + + if (!this.preAuthToken) { + throw new Error('Could not load authentication data required for third-party login. Please try again later.'); + } + } + } + + authUrl(queries) { + if (queries === void 0) { + queries = {}; } - return `${this.hostname}/${this.id}/connect?${new URLSearchParams(queries)}`; + const params = new URLSearchParams(queries); + + if (this.preAuthToken) { + params.set('uppyPreAuthToken', this.preAuthToken); + } + + return `${this.hostname}/${this.id}/connect?${params}`; } fileUrl(id) { return `${this.hostname}/${this.id}/get/${id}`; } - fetchPreAuthToken() { + async fetchPreAuthToken() { if (!this.companionKeysParams) { - return Promise.resolve(); + return; } - return this.post(`${this.id}/preauth/`, { - params: this.companionKeysParams - }).then(res => { + try { + const res = await this.post(`${this.id}/preauth/`, { + params: this.companionKeysParams + }); this.preAuthToken = res.token; - }).catch(err => { + } catch (err) { this.uppy.log(`[CompanionClient] unable to fetch preAuthToken ${err}`, 'warning'); - }); + } } list(directory) { @@ -1944,7 +1978,10 @@ module.exports = class Provider extends RequestClient { } logout() { - return this.get(`${this.id}/logout`).then(response => Promise.all([response, this.uppy.getPlugin(this.pluginId).storage.removeItem(this.tokenKey)])).then(([response]) => response); + return this.get(`${this.id}/logout`).then(response => Promise.all([response, this.uppy.getPlugin(this.pluginId).storage.removeItem(this.tokenKey)])).then(_ref2 => { + let [response] = _ref2; + return response; + }); } static initPlugin(plugin, opts, defaultOpts) { @@ -1980,7 +2017,7 @@ module.exports = class Provider extends RequestClient { } }; -},{"./RequestClient":9,"./tokenStorage":13}],9:[function(require,module,exports){ +},{"./RequestClient":10,"./tokenStorage":14}],10:[function(require,module,exports){ 'use strict'; var _class, _getPostResponseFunc, _getUrl, _errorHandler, _temp; @@ -2099,7 +2136,8 @@ module.exports = (_temp = (_getPostResponseFunc = /*#__PURE__*/_classPrivateFiel } preflightAndHeaders(path) { - return Promise.all([this.preflight(path), this.headers()]).then(([allowedHeaders, headers]) => { + return Promise.all([this.preflight(path), this.headers()]).then(_ref => { + let [allowedHeaders, headers] = _ref; // filter to keep only allowed Headers Object.keys(headers).forEach(header => { if (!allowedHeaders.includes(header.toLowerCase())) { @@ -2140,7 +2178,7 @@ module.exports = (_temp = (_getPostResponseFunc = /*#__PURE__*/_classPrivateFiel })).then(_classPrivateFieldLooseBase(this, _getPostResponseFunc)[_getPostResponseFunc](skipPostResponse)).then(handleJSONResponse).catch(_classPrivateFieldLooseBase(this, _errorHandler)[_errorHandler](method, path)); } -}), _class.VERSION = "2.0.3", _class.defaultHeaders = { +}), _class.VERSION = "2.0.5", _class.defaultHeaders = { Accept: 'application/json', 'Content-Type': 'application/json', 'Uppy-Versions': `@uppy/companion-client=${_class.VERSION}` @@ -2167,7 +2205,7 @@ function _errorHandler2(method, path) { return Promise.reject(err); }; } -},{"./AuthError":7,"@uppy/utils/lib/fetchWithNetworkError":31}],10:[function(require,module,exports){ +},{"./AuthError":8,"@uppy/utils/lib/fetchWithNetworkError":34}],11:[function(require,module,exports){ 'use strict'; const RequestClient = require('./RequestClient'); @@ -2195,9 +2233,7 @@ module.exports = class SearchProvider extends RequestClient { } }; -},{"./RequestClient":9}],11:[function(require,module,exports){ -"use strict"; - +},{"./RequestClient":10}],12:[function(require,module,exports){ var _queued, _emitter, _isOpen, _socket, _handleMessage; let _Symbol$for, _Symbol$for2; @@ -2315,7 +2351,7 @@ module.exports = (_queued = /*#__PURE__*/_classPrivateFieldLooseKey("queued"), _ } }); -},{"namespace-emitter":52}],12:[function(require,module,exports){ +},{"namespace-emitter":56}],13:[function(require,module,exports){ 'use strict'; /** * Manages communications with Companion @@ -2335,7 +2371,7 @@ module.exports = { SearchProvider, Socket }; -},{"./Provider":8,"./RequestClient":9,"./SearchProvider":10,"./Socket":11}],13:[function(require,module,exports){ +},{"./Provider":9,"./RequestClient":10,"./SearchProvider":11,"./Socket":12}],14:[function(require,module,exports){ 'use strict'; /** * This module serves as an Async wrapper for LocalStorage @@ -2358,9 +2394,7 @@ module.exports.removeItem = key => { resolve(); }); }; -},{}],14:[function(require,module,exports){ -"use strict"; - +},{}],15:[function(require,module,exports){ /** * Core plugin logic that all plugins share. * @@ -2372,7 +2406,11 @@ module.exports.removeItem = key => { const Translator = require('@uppy/utils/lib/Translator'); module.exports = class BasePlugin { - constructor(uppy, opts = {}) { + constructor(uppy, opts) { + if (opts === void 0) { + opts = {}; + } + this.uppy = uppy; this.opts = opts; } @@ -2450,9 +2488,169 @@ module.exports = class BasePlugin { afterUpdate() {} }; -},{"@uppy/utils/lib/Translator":28}],15:[function(require,module,exports){ -"use strict"; +},{"@uppy/utils/lib/Translator":31}],16:[function(require,module,exports){ +/* eslint-disable max-classes-per-file, class-methods-use-this */ +/* global AggregateError */ +const prettierBytes = require('@transloadit/prettier-bytes'); + +const match = require('mime-match'); + +const defaultOptions = { + maxFileSize: null, + minFileSize: null, + maxTotalFileSize: null, + maxNumberOfFiles: null, + minNumberOfFiles: null, + allowedFileTypes: null, + requiredMetaFields: [] +}; + +class RestrictionError extends Error { + constructor() { + super(...arguments); + this.isRestriction = true; + } + +} + +if (typeof AggregateError === 'undefined') { + // eslint-disable-next-line no-global-assign + // TODO: remove this "polyfill" in the next major. + globalThis.AggregateError = class AggregateError extends Error { + constructor(errors, message) { + super(message); + this.errors = errors; + } + + }; +} + +class Restricter { + constructor(getOpts, i18n) { + this.i18n = i18n; + + this.getOpts = () => { + const opts = getOpts(); + + if (opts.restrictions.allowedFileTypes != null && !Array.isArray(opts.restrictions.allowedFileTypes)) { + throw new TypeError('`restrictions.allowedFileTypes` must be an array'); + } + + return opts; + }; + } + + validate(file, files) { + const { + maxFileSize, + minFileSize, + maxTotalFileSize, + maxNumberOfFiles, + allowedFileTypes + } = this.getOpts().restrictions; + + if (maxNumberOfFiles && files.length + 1 > maxNumberOfFiles) { + throw new RestrictionError(`${this.i18n('youCanOnlyUploadX', { + smart_count: maxNumberOfFiles + })}`); + } + + if (allowedFileTypes) { + const isCorrectFileType = allowedFileTypes.some(type => { + // check if this is a mime-type + if (type.includes('/')) { + if (!file.type) return false; + return match(file.type.replace(/;.*?$/, ''), type); + } // otherwise this is likely an extension + + + if (type[0] === '.' && file.extension) { + return file.extension.toLowerCase() === type.substr(1).toLowerCase(); + } + + return false; + }); + + if (!isCorrectFileType) { + const allowedFileTypesString = allowedFileTypes.join(', '); + throw new RestrictionError(this.i18n('youCanOnlyUploadFileTypes', { + types: allowedFileTypesString + })); + } + } // We can't check maxTotalFileSize if the size is unknown. + + + if (maxTotalFileSize && file.size != null) { + const totalFilesSize = files.reduce((total, f) => total + f.size, file.size); + + if (totalFilesSize > maxTotalFileSize) { + throw new RestrictionError(this.i18n('exceedsSize', { + size: prettierBytes(maxTotalFileSize), + file: file.name + })); + } + } // We can't check maxFileSize if the size is unknown. + + + if (maxFileSize && file.size != null && file.size > maxFileSize) { + throw new RestrictionError(this.i18n('exceedsSize', { + size: prettierBytes(maxFileSize), + file: file.name + })); + } // We can't check minFileSize if the size is unknown. + + + if (minFileSize && file.size != null && file.size < minFileSize) { + throw new RestrictionError(this.i18n('inferiorSize', { + size: prettierBytes(minFileSize) + })); + } + } + + validateMinNumberOfFiles(files) { + const { + minNumberOfFiles + } = this.getOpts().restrictions; + + if (Object.keys(files).length < minNumberOfFiles) { + throw new RestrictionError(this.i18n('youHaveToAtLeastSelectX', { + smart_count: minNumberOfFiles + })); + } + } + + getMissingRequiredMetaFields(file) { + const error = new RestrictionError(this.i18n('missingRequiredMetaFieldOnFile', { + fileName: file.name + })); + const { + requiredMetaFields + } = this.getOpts().restrictions; // TODO: migrate to Object.hasOwn in the next major. + + const own = Object.prototype.hasOwnProperty; + const missingFields = []; + + for (const field of requiredMetaFields) { + if (!own.call(file.meta, field) || file.meta[field] === '') { + missingFields.push(field); + } + } + + return { + missingFields, + error + }; + } + +} + +module.exports = { + Restricter, + defaultOptions, + RestrictionError +}; +},{"@transloadit/prettier-bytes":1,"mime-match":55}],17:[function(require,module,exports){ function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } var id = 0; @@ -2477,7 +2675,11 @@ const BasePlugin = require('./BasePlugin'); function debounce(fn) { let calling = null; let latestArgs = null; - return (...args) => { + return function () { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + latestArgs = args; if (!calling) { @@ -2505,8 +2707,8 @@ function debounce(fn) { var _updateUI = /*#__PURE__*/_classPrivateFieldLooseKey("updateUI"); class UIPlugin extends BasePlugin { - constructor(...args) { - super(...args); + constructor() { + super(...arguments); Object.defineProperty(this, _updateUI, { writable: true, value: void 0 @@ -2617,7 +2819,9 @@ class UIPlugin extends BasePlugin { } module.exports = UIPlugin; -},{"./BasePlugin":14,"@uppy/utils/lib/findDOMElement":32,"preact":55}],16:[function(require,module,exports){ +},{"./BasePlugin":15,"@uppy/utils/lib/findDOMElement":35,"preact":58}],18:[function(require,module,exports){ +/* eslint-disable max-classes-per-file */ + /* global AggregateError */ 'use strict'; @@ -2635,14 +2839,10 @@ const ee = require('namespace-emitter'); const { nanoid -} = require('nanoid'); +} = require('nanoid/non-secure'); const throttle = require('lodash.throttle'); -const prettierBytes = require('@transloadit/prettier-bytes'); - -const match = require('mime-match'); - const DefaultStore = require('@uppy/store-default'); const getFileType = require('@uppy/utils/lib/getFileType'); @@ -2658,35 +2858,16 @@ const getFileName = require('./getFileName'); const { justErrorsLogger, debugLogger -} = require('./loggers'); // Exported from here. +} = require('./loggers'); +const { + Restricter, + defaultOptions: defaultRestrictionOptions, + RestrictionError +} = require('./Restricter'); -class RestrictionError extends Error { - constructor(...args) { - super(...args); - this.isRestriction = true; - } +const locale = require('./locale'); // Exported from here. -} - -if (typeof AggregateError === 'undefined') { - // eslint-disable-next-line no-global-assign - globalThis.AggregateError = class AggregateError extends Error { - constructor(message, errors) { - super(message); - this.errors = errors; - } - - }; -} - -class AggregateRestrictionError extends AggregateError { - constructor(...args) { - super(...args); - this.isRestriction = true; - } - -} /** * Uppy Core module. * Manages plugins, state updates, acts as an event bus, @@ -2696,6 +2877,8 @@ class AggregateRestrictionError extends AggregateError { var _plugins = /*#__PURE__*/_classPrivateFieldLooseKey("plugins"); +var _restricter = /*#__PURE__*/_classPrivateFieldLooseKey("restricter"); + var _storeUnsubscribe = /*#__PURE__*/_classPrivateFieldLooseKey("storeUnsubscribe"); var _emitter = /*#__PURE__*/_classPrivateFieldLooseKey("emitter"); @@ -2706,14 +2889,12 @@ var _uploaders = /*#__PURE__*/_classPrivateFieldLooseKey("uploaders"); var _postProcessors = /*#__PURE__*/_classPrivateFieldLooseKey("postProcessors"); -var _checkRestrictions = /*#__PURE__*/_classPrivateFieldLooseKey("checkRestrictions"); +var _informAndEmit = /*#__PURE__*/_classPrivateFieldLooseKey("informAndEmit"); -var _checkMinNumberOfFiles = /*#__PURE__*/_classPrivateFieldLooseKey("checkMinNumberOfFiles"); +var _checkRequiredMetaFieldsOnFile = /*#__PURE__*/_classPrivateFieldLooseKey("checkRequiredMetaFieldsOnFile"); var _checkRequiredMetaFields = /*#__PURE__*/_classPrivateFieldLooseKey("checkRequiredMetaFields"); -var _showOrLogErrorAndThrow = /*#__PURE__*/_classPrivateFieldLooseKey("showOrLogErrorAndThrow"); - var _assertNewUploadAllowed = /*#__PURE__*/_classPrivateFieldLooseKey("assertNewUploadAllowed"); var _checkAndCreateFileStateObject = /*#__PURE__*/_classPrivateFieldLooseKey("checkAndCreateFileStateObject"); @@ -2770,22 +2951,23 @@ class Uppy { Object.defineProperty(this, _assertNewUploadAllowed, { value: _assertNewUploadAllowed2 }); - Object.defineProperty(this, _showOrLogErrorAndThrow, { - value: _showOrLogErrorAndThrow2 - }); Object.defineProperty(this, _checkRequiredMetaFields, { value: _checkRequiredMetaFields2 }); - Object.defineProperty(this, _checkMinNumberOfFiles, { - value: _checkMinNumberOfFiles2 + Object.defineProperty(this, _checkRequiredMetaFieldsOnFile, { + value: _checkRequiredMetaFieldsOnFile2 }); - Object.defineProperty(this, _checkRestrictions, { - value: _checkRestrictions2 + Object.defineProperty(this, _informAndEmit, { + value: _informAndEmit2 }); Object.defineProperty(this, _plugins, { writable: true, value: Object.create(null) }); + Object.defineProperty(this, _restricter, { + writable: true, + value: void 0 + }); Object.defineProperty(this, _storeUnsubscribe, { writable: true, value: void 0 @@ -2810,60 +2992,7 @@ class Uppy { writable: true, value: this.updateOnlineStatus.bind(this) }); - this.defaultLocale = { - strings: { - addBulkFilesFailed: { - 0: 'Failed to add %{smart_count} file due to an internal error', - 1: 'Failed to add %{smart_count} files due to internal errors' - }, - youCanOnlyUploadX: { - 0: 'You can only upload %{smart_count} file', - 1: 'You can only upload %{smart_count} files' - }, - youHaveToAtLeastSelectX: { - 0: 'You have to select at least %{smart_count} file', - 1: 'You have to select at least %{smart_count} files' - }, - exceedsSize: '%{file} exceeds maximum allowed size of %{size}', - missingRequiredMetaField: 'Missing required meta fields', - missingRequiredMetaFieldOnFile: 'Missing required meta fields in %{fileName}', - inferiorSize: 'This file is smaller than the allowed size of %{size}', - youCanOnlyUploadFileTypes: 'You can only upload: %{types}', - noMoreFilesAllowed: 'Cannot add more files', - noDuplicates: 'Cannot add the duplicate file \'%{fileName}\', it already exists', - companionError: 'Connection with Companion failed', - authAborted: 'Authentication aborted', - companionUnauthorizeHint: 'To unauthorize to your %{provider} account, please go to %{url}', - failedToUpload: 'Failed to upload %{file}', - noInternetConnection: 'No Internet connection', - connectedToInternet: 'Connected to the Internet', - // Strings for remote providers - noFilesFound: 'You have no files or folders here', - selectX: { - 0: 'Select %{smart_count}', - 1: 'Select %{smart_count}' - }, - allFilesFromFolderNamed: 'All files from folder %{name}', - openFolderNamed: 'Open folder %{name}', - cancel: 'Cancel', - logOut: 'Log out', - filter: 'Filter', - resetFilter: 'Reset filter', - loading: 'Loading...', - authenticateWithTitle: 'Please authenticate with %{pluginName} to select files', - authenticateWith: 'Connect to %{pluginName}', - signInWithGoogle: 'Sign in with Google', - searchImages: 'Search for images', - enterTextToSearch: 'Enter text to search for images', - backToSearch: 'Back to Search', - emptyFolderAdded: 'No files were added from empty folder', - folderAlreadyAdded: 'The folder "%{folder}" was already added', - folderAdded: { - 0: 'Added %{smart_count} file from %{folder}', - 1: 'Added %{smart_count} files from %{folder}' - } - } - }; + this.defaultLocale = locale; const defaultOptions = { id: 'uppy', autoProceed: false, @@ -2874,15 +3003,7 @@ class Uppy { allowMultipleUploads: true, allowMultipleUploadBatches: true, debug: false, - restrictions: { - maxFileSize: null, - minFileSize: null, - maxTotalFileSize: null, - maxNumberOfFiles: null, - minNumberOfFiles: null, - allowedFileTypes: null, - requiredMetaFields: [] - }, + restrictions: defaultRestrictionOptions, meta: {}, onBeforeFileAdded: currentFile => currentFile, onBeforeUpload: files => files, @@ -2907,11 +3028,6 @@ class Uppy { } this.log(`Using Core v${this.constructor.VERSION}`); - - if (this.opts.restrictions.allowedFileTypes && this.opts.restrictions.allowedFileTypes !== null && !Array.isArray(this.opts.restrictions.allowedFileTypes)) { - throw new TypeError('`restrictions.allowedFileTypes` must be an array'); - } - this.i18nInit(); // ___Why throttle at 500ms? // - We must throttle at >250ms for superfocus in Dashboard to work well // (because animation takes 0.25s, and we want to wait for all animations to be over before refocusing). @@ -2941,6 +3057,7 @@ class Uppy { info: [], recoveredState: null }); + _classPrivateFieldLooseBase(this, _restricter)[_restricter] = new Restricter(() => this.opts, this.i18n); _classPrivateFieldLooseBase(this, _storeUnsubscribe)[_storeUnsubscribe] = this.store.subscribe((prevState, nextState, patch) => { this.emit('state-update', prevState, nextState, patch); this.updateAll(nextState); @@ -2953,7 +3070,11 @@ class Uppy { _classPrivateFieldLooseBase(this, _addListeners)[_addListeners](); } - emit(event, ...args) { + emit(event) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + _classPrivateFieldLooseBase(this, _emitter)[_emitter].emit(event, ...args); } @@ -3185,9 +3306,12 @@ class Uppy { error } = this.getState(); const files = Object.values(filesObject); - const inProgressFiles = files.filter(({ - progress - }) => !progress.uploadComplete && progress.uploadStarted); + const inProgressFiles = files.filter(_ref => { + let { + progress + } = _ref; + return !progress.uploadComplete && progress.uploadStarted; + }); const newFiles = files.filter(file => !file.progress.uploadStarted); const startedFiles = files.filter(file => file.progress.uploadStarted || file.progress.preprocess || file.progress.postprocess); const uploadStartedFiles = files.filter(file => file.progress.uploadStarted); @@ -3214,19 +3338,28 @@ class Uppy { isSomeGhost: files.some(file => file.isGhost) }; } - /** - * A public wrapper for _checkRestrictions — checks if a file passes a set of restrictions. - * For use in UI pluigins (like Providers), to disallow selecting files that won’t pass restrictions. - * - * @param {object} file object to check - * @param {Array} [files] array to check maxNumberOfFiles and maxTotalFileSize - * @returns {object} { result: true/false, reason: why file didn’t pass restrictions } - */ + /* + * @constructs + * @param { Error } error + * @param { undefined } file + */ + + /* + * @constructs + * @param { RestrictionError } error + * @param { UppyFile | undefined } file + */ validateRestrictions(file, files) { + if (files === void 0) { + files = this.getFiles(); + } + + // TODO: directly return the Restriction error in next major version. + // we create RestrictionError's just to discard immediately, which doesn't make sense. try { - _classPrivateFieldLooseBase(this, _checkRestrictions)[_checkRestrictions](file, files); + _classPrivateFieldLooseBase(this, _restricter)[_restricter].validate(file, files); return { result: true @@ -3238,15 +3371,6 @@ class Uppy { }; } } - /** - * Check if file passes a set of restrictions set in options: maxFileSize, minFileSize, - * maxNumberOfFiles and allowedFileTypes. - * - * @param {object} file object to check - * @param {Array} [files] array to check maxNumberOfFiles and maxTotalFileSize - * @private - */ - checkIfFileAlreadyExists(fileID) { const { @@ -3451,7 +3575,11 @@ class Uppy { } } - removeFile(fileID, reason = null) { + removeFile(fileID, reason) { + if (reason === void 0) { + reason = null; + } + this.removeFiles([fileID], reason); } @@ -3837,7 +3965,15 @@ class Uppy { */ - info(message, type = 'info', duration = 3000) { + info(message, type, duration) { + if (type === void 0) { + type = 'info'; + } + + if (duration === void 0) { + duration = 3000; + } + const isComplexMessage = typeof message === 'object'; this.setState({ info: [...this.getState().info, { @@ -3901,8 +4037,8 @@ class Uppy { */ - [_Symbol$for2](...args) { - return _classPrivateFieldLooseBase(this, _createUpload)[_createUpload](...args); + [_Symbol$for2]() { + return _classPrivateFieldLooseBase(this, _createUpload)[_createUpload](...arguments); } /** @@ -3968,12 +4104,19 @@ class Uppy { }); } - return Promise.resolve().then(() => { - _classPrivateFieldLooseBase(this, _checkMinNumberOfFiles)[_checkMinNumberOfFiles](files); + return Promise.resolve().then(() => _classPrivateFieldLooseBase(this, _restricter)[_restricter].validateMinNumberOfFiles(files)).catch(err => { + _classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](err); - _classPrivateFieldLooseBase(this, _checkRequiredMetaFields)[_checkRequiredMetaFields](files); + throw err; + }).then(() => { + if (!_classPrivateFieldLooseBase(this, _checkRequiredMetaFields)[_checkRequiredMetaFields](files)) { + throw new RestrictionError(this.i18n('missingRequiredMetaField')); + } }).catch(err => { - _classPrivateFieldLooseBase(this, _showOrLogErrorAndThrow)[_showOrLogErrorAndThrow](err); + // Doing this in a separate catch because we already emited and logged + // all the errors in `checkRequiredMetaFields` so we only throw a generic + // missing fields error here. + throw err; }).then(() => { const { currentUploads @@ -3993,170 +4136,61 @@ class Uppy { return _classPrivateFieldLooseBase(this, _runUpload)[_runUpload](uploadID); }).catch(err => { - _classPrivateFieldLooseBase(this, _showOrLogErrorAndThrow)[_showOrLogErrorAndThrow](err, { - showInformer: false - }); + this.emit('error', err); + this.log(err, 'error'); + throw err; }); } } -function _checkRestrictions2(file, files = this.getFiles()) { +function _informAndEmit2(error, file) { const { - maxFileSize, - minFileSize, - maxTotalFileSize, - maxNumberOfFiles, - allowedFileTypes - } = this.opts.restrictions; + message, + details = '' + } = error; - if (maxNumberOfFiles) { - if (files.length + 1 > maxNumberOfFiles) { - throw new RestrictionError(`${this.i18n('youCanOnlyUploadX', { - smart_count: maxNumberOfFiles - })}`); - } + if (error.isRestriction) { + this.emit('restriction-failed', file, error); + } else { + this.emit('error', error); } - if (allowedFileTypes) { - const isCorrectFileType = allowedFileTypes.some(type => { - // check if this is a mime-type - if (type.indexOf('/') > -1) { - if (!file.type) return false; - return match(file.type.replace(/;.*?$/, ''), type); - } // otherwise this is likely an extension - - - if (type[0] === '.' && file.extension) { - return file.extension.toLowerCase() === type.substr(1).toLowerCase(); - } - - return false; - }); - - if (!isCorrectFileType) { - const allowedFileTypesString = allowedFileTypes.join(', '); - throw new RestrictionError(this.i18n('youCanOnlyUploadFileTypes', { - types: allowedFileTypesString - })); - } - } // We can't check maxTotalFileSize if the size is unknown. - - - if (maxTotalFileSize && file.size != null) { - let totalFilesSize = 0; - totalFilesSize += file.size; - files.forEach(f => { - totalFilesSize += f.size; - }); - - if (totalFilesSize > maxTotalFileSize) { - throw new RestrictionError(this.i18n('exceedsSize', { - size: prettierBytes(maxTotalFileSize), - file: file.name - })); - } - } // We can't check maxFileSize if the size is unknown. - - - if (maxFileSize && file.size != null) { - if (file.size > maxFileSize) { - throw new RestrictionError(this.i18n('exceedsSize', { - size: prettierBytes(maxFileSize), - file: file.name - })); - } - } // We can't check minFileSize if the size is unknown. - - - if (minFileSize && file.size != null) { - if (file.size < minFileSize) { - throw new RestrictionError(this.i18n('inferiorSize', { - size: prettierBytes(minFileSize) - })); - } - } + this.info({ + message, + details + }, 'error', this.opts.infoTimeout); + this.log(`${message} ${details}`.trim(), 'error'); } -function _checkMinNumberOfFiles2(files) { +function _checkRequiredMetaFieldsOnFile2(file) { const { - minNumberOfFiles - } = this.opts.restrictions; + missingFields, + error + } = _classPrivateFieldLooseBase(this, _restricter)[_restricter].getMissingRequiredMetaFields(file); - if (Object.keys(files).length < minNumberOfFiles) { - throw new RestrictionError(`${this.i18n('youHaveToAtLeastSelectX', { - smart_count: minNumberOfFiles - })}`); + if (missingFields.length > 0) { + this.setFileState(file.id, { + missingRequiredMetaFields: missingFields + }); + this.log(error.message); + this.emit('restriction-failed', file, error); + return false; } + + return true; } function _checkRequiredMetaFields2(files) { - const { - requiredMetaFields - } = this.opts.restrictions; - const { - hasOwnProperty - } = Object.prototype; - const errors = []; + let success = true; - for (const fileID of Object.keys(files)) { - const file = this.getFile(fileID); - - for (let i = 0; i < requiredMetaFields.length; i++) { - if (!hasOwnProperty.call(file.meta, requiredMetaFields[i]) || file.meta[requiredMetaFields[i]] === '') { - const err = new RestrictionError(`${this.i18n('missingRequiredMetaFieldOnFile', { - fileName: file.name - })}`); - errors.push(err); - - _classPrivateFieldLooseBase(this, _showOrLogErrorAndThrow)[_showOrLogErrorAndThrow](err, { - file, - showInformer: false, - throwErr: false - }); - } + for (const file of Object.values(files)) { + if (!_classPrivateFieldLooseBase(this, _checkRequiredMetaFieldsOnFile)[_checkRequiredMetaFieldsOnFile](file)) { + success = false; } } - if (errors.length) { - throw new AggregateRestrictionError(`${this.i18n('missingRequiredMetaField')}`, errors); - } -} - -function _showOrLogErrorAndThrow2(err, { - showInformer = true, - file = null, - throwErr = true -} = {}) { - const message = typeof err === 'object' ? err.message : err; - const details = typeof err === 'object' && err.details ? err.details : ''; // Restriction errors should be logged, but not as errors, - // as they are expected and shown in the UI. - - let logMessageWithDetails = message; - - if (details) { - logMessageWithDetails += ` ${details}`; - } - - if (err.isRestriction) { - this.log(logMessageWithDetails); - this.emit('restriction-failed', file, err); - } else { - this.log(logMessageWithDetails, 'error'); - } // Sometimes informer has to be shown manually by the developer, - // for example, in `onBeforeFileAdded`. - - - if (showInformer) { - this.info({ - message, - details - }, 'error', this.opts.infoTimeout); - } - - if (throwErr) { - throw typeof err === 'object' ? err : new Error(err); - } + return success; } function _assertNewUploadAllowed2(file) { @@ -4165,9 +4199,11 @@ function _assertNewUploadAllowed2(file) { } = this.getState(); if (allowNewUpload === false) { - _classPrivateFieldLooseBase(this, _showOrLogErrorAndThrow)[_showOrLogErrorAndThrow](new RestrictionError(this.i18n('noMoreFilesAllowed')), { - file - }); + const error = new RestrictionError(this.i18n('noMoreFilesAllowed')); + + _classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](error, file); + + throw error; } } @@ -4185,9 +4221,9 @@ function _checkAndCreateFileStateObject2(files, fileDescriptor) { fileName })); - _classPrivateFieldLooseBase(this, _showOrLogErrorAndThrow)[_showOrLogErrorAndThrow](error, { - file: fileDescriptor - }); + _classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](error, fileDescriptor); + + throw error; } const meta = fileDescriptor.meta || {}; @@ -4221,10 +4257,9 @@ function _checkAndCreateFileStateObject2(files, fileDescriptor) { if (onBeforeFileAddedResult === false) { // Don’t show UI info for this error, as it should be done by the developer - _classPrivateFieldLooseBase(this, _showOrLogErrorAndThrow)[_showOrLogErrorAndThrow](new RestrictionError('Cannot add the file because onBeforeFileAdded returned false.'), { - showInformer: false, - fileDescriptor - }); + const error = new RestrictionError('Cannot add the file because onBeforeFileAdded returned false.'); + this.emit('restriction-failed', fileDescriptor, error); + throw error; } else if (typeof onBeforeFileAddedResult === 'object' && onBeforeFileAddedResult !== null) { newFile = onBeforeFileAddedResult; } @@ -4232,11 +4267,11 @@ function _checkAndCreateFileStateObject2(files, fileDescriptor) { try { const filesArray = Object.keys(files).map(i => files[i]); - _classPrivateFieldLooseBase(this, _checkRestrictions)[_checkRestrictions](newFile, filesArray); + _classPrivateFieldLooseBase(this, _restricter)[_restricter].validate(newFile, filesArray); } catch (err) { - _classPrivateFieldLooseBase(this, _showOrLogErrorAndThrow)[_showOrLogErrorAndThrow](err, { - file: newFile - }); + _classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](err, newFile); + + throw err; } return newFile; @@ -4296,13 +4331,9 @@ function _addListeners2() { file: file.name }); - _classPrivateFieldLooseBase(this, _showOrLogErrorAndThrow)[_showOrLogErrorAndThrow](newError, { - throwErr: false - }); + _classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](newError); } else { - _classPrivateFieldLooseBase(this, _showOrLogErrorAndThrow)[_showOrLogErrorAndThrow](error, { - throwErr: false - }); + _classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](error); } }); this.on('upload', () => { @@ -4418,6 +4449,11 @@ function _addListeners2() { this.on('restored', () => { // Files may have changed--ensure progress is still accurate. this.calculateTotalProgress(); + }); + this.on('dashboard:file-edit-complete', file => { + if (file) { + _classPrivateFieldLooseBase(this, _checkRequiredMetaFieldsOnFile)[_checkRequiredMetaFieldsOnFile](file); + } }); // show informer if offline if (typeof window !== 'undefined' && window.addEventListener) { @@ -4427,7 +4463,11 @@ function _addListeners2() { } } -function _createUpload2(fileIDs, opts = {}) { +function _createUpload2(fileIDs, opts) { + if (opts === void 0) { + opts = {}; + } + // uppy.retryAll sets this to true — when retrying we want to ignore `allowNewUpload: false` const { forceAllowNewUpload = false @@ -4506,8 +4546,6 @@ async function _runUpload2(uploadID) { currentUpload = currentUploads[uploadID]; } } catch (err) { - this.emit('error', err); - _classPrivateFieldLooseBase(this, _removeUpload)[_removeUpload](uploadID); throw err; @@ -4565,11 +4603,9 @@ async function _runUpload2(uploadID) { return result; } -Uppy.VERSION = "2.1.1"; +Uppy.VERSION = "2.1.6"; module.exports = Uppy; -},{"./getFileName":17,"./loggers":19,"./supportsUploadProgress":20,"@transloadit/prettier-bytes":1,"@uppy/store-default":22,"@uppy/utils/lib/Translator":28,"@uppy/utils/lib/generateFileID":33,"@uppy/utils/lib/getFileNameAndExtension":39,"@uppy/utils/lib/getFileType":40,"lodash.throttle":50,"mime-match":51,"namespace-emitter":52,"nanoid":53}],17:[function(require,module,exports){ -"use strict"; - +},{"./Restricter":16,"./getFileName":19,"./locale":21,"./loggers":22,"./supportsUploadProgress":23,"@uppy/store-default":25,"@uppy/utils/lib/Translator":31,"@uppy/utils/lib/generateFileID":36,"@uppy/utils/lib/getFileNameAndExtension":42,"@uppy/utils/lib/getFileType":43,"lodash.throttle":54,"namespace-emitter":56,"nanoid/non-secure":57}],19:[function(require,module,exports){ module.exports = function getFileName(fileType, fileDescriptor) { if (fileDescriptor.name) { return fileDescriptor.name; @@ -4581,7 +4617,7 @@ module.exports = function getFileName(fileType, fileDescriptor) { return 'noname'; }; -},{}],18:[function(require,module,exports){ +},{}],20:[function(require,module,exports){ 'use strict'; const Uppy = require('./Uppy'); @@ -4599,9 +4635,62 @@ module.exports.Uppy = Uppy; module.exports.UIPlugin = UIPlugin; module.exports.BasePlugin = BasePlugin; module.exports.debugLogger = debugLogger; -},{"./BasePlugin":14,"./UIPlugin":15,"./Uppy":16,"./loggers":19}],19:[function(require,module,exports){ -"use strict"; - +},{"./BasePlugin":15,"./UIPlugin":17,"./Uppy":18,"./loggers":22}],21:[function(require,module,exports){ +module.exports = { + strings: { + addBulkFilesFailed: { + 0: 'Failed to add %{smart_count} file due to an internal error', + 1: 'Failed to add %{smart_count} files due to internal errors' + }, + youCanOnlyUploadX: { + 0: 'You can only upload %{smart_count} file', + 1: 'You can only upload %{smart_count} files' + }, + youHaveToAtLeastSelectX: { + 0: 'You have to select at least %{smart_count} file', + 1: 'You have to select at least %{smart_count} files' + }, + exceedsSize: '%{file} exceeds maximum allowed size of %{size}', + missingRequiredMetaField: 'Missing required meta fields', + missingRequiredMetaFieldOnFile: 'Missing required meta fields in %{fileName}', + inferiorSize: 'This file is smaller than the allowed size of %{size}', + youCanOnlyUploadFileTypes: 'You can only upload: %{types}', + noMoreFilesAllowed: 'Cannot add more files', + noDuplicates: "Cannot add the duplicate file '%{fileName}', it already exists", + companionError: 'Connection with Companion failed', + authAborted: 'Authentication aborted', + companionUnauthorizeHint: 'To unauthorize to your %{provider} account, please go to %{url}', + failedToUpload: 'Failed to upload %{file}', + noInternetConnection: 'No Internet connection', + connectedToInternet: 'Connected to the Internet', + // Strings for remote providers + noFilesFound: 'You have no files or folders here', + selectX: { + 0: 'Select %{smart_count}', + 1: 'Select %{smart_count}' + }, + allFilesFromFolderNamed: 'All files from folder %{name}', + openFolderNamed: 'Open folder %{name}', + cancel: 'Cancel', + logOut: 'Log out', + filter: 'Filter', + resetFilter: 'Reset filter', + loading: 'Loading...', + authenticateWithTitle: 'Please authenticate with %{pluginName} to select files', + authenticateWith: 'Connect to %{pluginName}', + signInWithGoogle: 'Sign in with Google', + searchImages: 'Search for images', + enterTextToSearch: 'Enter text to search for images', + search: 'Search', + emptyFolderAdded: 'No files were added from empty folder', + folderAlreadyAdded: 'The folder "%{folder}" was already added', + folderAdded: { + 0: 'Added %{smart_count} file from %{folder}', + 1: 'Added %{smart_count} files from %{folder}' + } + } +}; +},{}],22:[function(require,module,exports){ /* eslint-disable no-console */ const getTimeStamp = require('@uppy/utils/lib/getTimeStamp'); // Swallow all logs, except errors. // default if logger is not set or debug: false @@ -4610,22 +4699,44 @@ const getTimeStamp = require('@uppy/utils/lib/getTimeStamp'); // Swallow all log const justErrorsLogger = { debug: () => {}, warn: () => {}, - error: (...args) => console.error(`[Uppy] [${getTimeStamp()}]`, ...args) + error: function () { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + return console.error(`[Uppy] [${getTimeStamp()}]`, ...args); + } }; // Print logs to console with namespace + timestamp, // set by logger: Uppy.debugLogger or debug: true const debugLogger = { - debug: (...args) => console.debug(`[Uppy] [${getTimeStamp()}]`, ...args), - warn: (...args) => console.warn(`[Uppy] [${getTimeStamp()}]`, ...args), - error: (...args) => console.error(`[Uppy] [${getTimeStamp()}]`, ...args) + debug: function () { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + return console.debug(`[Uppy] [${getTimeStamp()}]`, ...args); + }, + warn: function () { + for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + args[_key3] = arguments[_key3]; + } + + return console.warn(`[Uppy] [${getTimeStamp()}]`, ...args); + }, + error: function () { + for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { + args[_key4] = arguments[_key4]; + } + + return console.error(`[Uppy] [${getTimeStamp()}]`, ...args); + } }; module.exports = { justErrorsLogger, debugLogger }; -},{"@uppy/utils/lib/getTimeStamp":42}],20:[function(require,module,exports){ -"use strict"; - +},{"@uppy/utils/lib/getTimeStamp":45}],23:[function(require,module,exports){ // Edge 15.x does not fire 'progress' events on uploads. // See https://github.com/transloadit/uppy/issues/945 // And https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12224510/ @@ -4659,9 +4770,7 @@ module.exports = function supportsUploadProgress(userAgent) { return false; }; -},{}],21:[function(require,module,exports){ -"use strict"; - +},{}],24:[function(require,module,exports){ var _class, _temp; const BasePlugin = require('@uppy/core/lib/BasePlugin'); @@ -4699,36 +4808,69 @@ module.exports = (_temp = _class = class DropTarget extends BasePlugin { } }; + this.isFileTransfer = event => { + var _event$dataTransfer$t; + + const transferTypes = (_event$dataTransfer$t = event.dataTransfer.types) != null ? _event$dataTransfer$t : []; + return transferTypes.some(type => type === 'Files'); + }; + this.handleDrop = async event => { var _this$opts$onDrop, _this$opts; + if (!this.isFileTransfer(event)) { + return; + } + event.preventDefault(); event.stopPropagation(); - clearTimeout(this.removeDragOverClassTimeout); // 2. Remove dragover class + clearTimeout(this.removeDragOverClassTimeout); // Remove dragover class event.currentTarget.classList.remove('uppy-is-drag-over'); this.setPluginState({ isDraggingOver: false - }); // 3. Add all dropped files + }); // Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root - this.uppy.log('[DropTarget] Files were dropped'); + this.uppy.iteratePlugins(plugin => { + if (plugin.type === 'acquirer') { + // Every Plugin with .type acquirer can define handleRootDrop(event) + plugin.handleRootDrop == null ? void 0 : plugin.handleRootDrop(event); + } + }); // Add all dropped files, handle errors + + let executedDropErrorOnce = false; const logDropError = error => { - this.uppy.log(error, 'error'); + this.uppy.log(error, 'error'); // In practice all drop errors are most likely the same, + // so let's just show one to avoid overwhelming the user + + if (!executedDropErrorOnce) { + this.uppy.info(error.message, 'error'); + executedDropErrorOnce = true; + } }; const files = await getDroppedFiles(event.dataTransfer, { logDropError }); - this.addFiles(files); + + if (files.length > 0) { + this.uppy.log('[DropTarget] Files were dropped'); + this.addFiles(files); + } + (_this$opts$onDrop = (_this$opts = this.opts).onDrop) == null ? void 0 : _this$opts$onDrop.call(_this$opts, event); }; this.handleDragOver = event => { var _this$opts$onDragOver, _this$opts2; + if (!this.isFileTransfer(event)) { + return; + } + event.preventDefault(); - event.stopPropagation(); // 1. Add a small (+) icon on drop + event.stopPropagation(); // Add a small (+) icon on drop // (and prevent browsers from interpreting this as files being _moved_ into the browser, // https://github.com/transloadit/uppy/issues/1978) @@ -4744,6 +4886,10 @@ module.exports = (_temp = _class = class DropTarget extends BasePlugin { this.handleDragLeave = event => { var _this$opts$onDragLeav, _this$opts3; + if (!this.isFileTransfer(event)) { + return; + } + event.preventDefault(); event.stopPropagation(); const { @@ -4818,8 +4964,8 @@ module.exports = (_temp = _class = class DropTarget extends BasePlugin { this.removeListeners(); } -}, _class.VERSION = "1.1.1", _temp); -},{"@uppy/core/lib/BasePlugin":14,"@uppy/utils/lib/getDroppedFiles":34,"@uppy/utils/lib/toArray":48}],22:[function(require,module,exports){ +}, _class.VERSION = "1.1.2", _temp); +},{"@uppy/core/lib/BasePlugin":15,"@uppy/utils/lib/getDroppedFiles":37,"@uppy/utils/lib/toArray":51}],25:[function(require,module,exports){ "use strict"; function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } @@ -4867,18 +5013,22 @@ class DefaultStore { } -function _publish2(...args) { +function _publish2() { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + this.callbacks.forEach(listener => { listener(...args); }); } -DefaultStore.VERSION = "2.0.2"; +DefaultStore.VERSION = "2.0.3"; module.exports = function defaultStore() { return new DefaultStore(); }; -},{}],23:[function(require,module,exports){ +},{}],26:[function(require,module,exports){ "use strict"; /** @@ -4887,8 +5037,14 @@ module.exports = function defaultStore() { exports.AbortController = globalThis.AbortController; exports.AbortSignal = globalThis.AbortSignal; -exports.createAbortError = (message = 'Aborted') => new DOMException(message, 'AbortError'); -},{}],24:[function(require,module,exports){ +exports.createAbortError = function (message) { + if (message === void 0) { + message = 'Aborted'; + } + + return new DOMException(message, 'AbortError'); +}; +},{}],27:[function(require,module,exports){ "use strict"; var _emitter, _events; @@ -4929,11 +5085,15 @@ module.exports = (_emitter = /*#__PURE__*/_classPrivateFieldLooseKey("emitter"), } }); -},{}],25:[function(require,module,exports){ +},{}],28:[function(require,module,exports){ "use strict"; class NetworkError extends Error { - constructor(error, xhr = null) { + constructor(error, xhr) { + if (xhr === void 0) { + xhr = null; + } + super(`This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.`); this.cause = error; this.isNetworkError = true; @@ -4943,7 +5103,7 @@ class NetworkError extends Error { } module.exports = NetworkError; -},{}],26:[function(require,module,exports){ +},{}],29:[function(require,module,exports){ "use strict"; function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } @@ -5011,7 +5171,7 @@ class ProgressTimeout { } module.exports = ProgressTimeout; -},{}],27:[function(require,module,exports){ +},{}],30:[function(require,module,exports){ "use strict"; function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } @@ -5028,6 +5188,16 @@ var _activeRequests = /*#__PURE__*/_classPrivateFieldLooseKey("activeRequests"); var _queuedHandlers = /*#__PURE__*/_classPrivateFieldLooseKey("queuedHandlers"); +var _paused = /*#__PURE__*/_classPrivateFieldLooseKey("paused"); + +var _pauseTimer = /*#__PURE__*/_classPrivateFieldLooseKey("pauseTimer"); + +var _downLimit = /*#__PURE__*/_classPrivateFieldLooseKey("downLimit"); + +var _upperLimit = /*#__PURE__*/_classPrivateFieldLooseKey("upperLimit"); + +var _rateLimitingTimer = /*#__PURE__*/_classPrivateFieldLooseKey("rateLimitingTimer"); + var _call = /*#__PURE__*/_classPrivateFieldLooseKey("call"); var _queueNext = /*#__PURE__*/_classPrivateFieldLooseKey("queueNext"); @@ -5038,6 +5208,10 @@ var _queue = /*#__PURE__*/_classPrivateFieldLooseKey("queue"); var _dequeue = /*#__PURE__*/_classPrivateFieldLooseKey("dequeue"); +var _resume = /*#__PURE__*/_classPrivateFieldLooseKey("resume"); + +var _increaseLimit = /*#__PURE__*/_classPrivateFieldLooseKey("increaseLimit"); + class RateLimitedQueue { constructor(limit) { Object.defineProperty(this, _dequeue, { @@ -5063,6 +5237,52 @@ class RateLimitedQueue { writable: true, value: [] }); + Object.defineProperty(this, _paused, { + writable: true, + value: false + }); + Object.defineProperty(this, _pauseTimer, { + writable: true, + value: void 0 + }); + Object.defineProperty(this, _downLimit, { + writable: true, + value: 1 + }); + Object.defineProperty(this, _upperLimit, { + writable: true, + value: void 0 + }); + Object.defineProperty(this, _rateLimitingTimer, { + writable: true, + value: void 0 + }); + Object.defineProperty(this, _resume, { + writable: true, + value: () => this.resume() + }); + Object.defineProperty(this, _increaseLimit, { + writable: true, + value: () => { + if (_classPrivateFieldLooseBase(this, _paused)[_paused]) { + _classPrivateFieldLooseBase(this, _rateLimitingTimer)[_rateLimitingTimer] = setTimeout(_classPrivateFieldLooseBase(this, _increaseLimit)[_increaseLimit], 0); + return; + } + + _classPrivateFieldLooseBase(this, _downLimit)[_downLimit] = this.limit; + this.limit = Math.ceil((_classPrivateFieldLooseBase(this, _upperLimit)[_upperLimit] + _classPrivateFieldLooseBase(this, _downLimit)[_downLimit]) / 2); + + for (let i = _classPrivateFieldLooseBase(this, _downLimit)[_downLimit]; i <= this.limit; i++) { + _classPrivateFieldLooseBase(this, _queueNext)[_queueNext](); + } + + if (_classPrivateFieldLooseBase(this, _upperLimit)[_upperLimit] - _classPrivateFieldLooseBase(this, _downLimit)[_downLimit] > 3) { + _classPrivateFieldLooseBase(this, _rateLimitingTimer)[_rateLimitingTimer] = setTimeout(_classPrivateFieldLooseBase(this, _increaseLimit)[_increaseLimit], 2000); + } else { + _classPrivateFieldLooseBase(this, _downLimit)[_downLimit] = Math.floor(_classPrivateFieldLooseBase(this, _downLimit)[_downLimit] / 2); + } + } + }); if (typeof limit !== 'number' || limit === 0) { this.limit = Infinity; @@ -5072,7 +5292,7 @@ class RateLimitedQueue { } run(fn, queueOptions) { - if (_classPrivateFieldLooseBase(this, _activeRequests)[_activeRequests] < this.limit) { + if (!_classPrivateFieldLooseBase(this, _paused)[_paused] && _classPrivateFieldLooseBase(this, _activeRequests)[_activeRequests] < this.limit) { return _classPrivateFieldLooseBase(this, _call)[_call](fn); } @@ -5080,10 +5300,16 @@ class RateLimitedQueue { } wrapPromiseFunction(fn, queueOptions) { - return (...args) => { + var _this = this; + + return function () { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + let queuedRequest; const outerPromise = new Promise((resolve, reject) => { - queuedRequest = this.run(() => { + queuedRequest = _this.run(() => { let cancelError; let innerPromise; @@ -5122,6 +5348,60 @@ class RateLimitedQueue { }; } + resume() { + _classPrivateFieldLooseBase(this, _paused)[_paused] = false; + clearTimeout(_classPrivateFieldLooseBase(this, _pauseTimer)[_pauseTimer]); + + for (let i = 0; i < this.limit; i++) { + _classPrivateFieldLooseBase(this, _queueNext)[_queueNext](); + } + } + + /** + * Freezes the queue for a while or indefinitely. + * + * @param {number | null } [duration] Duration for the pause to happen, in milliseconds. + * If omitted, the queue won't resume automatically. + */ + pause(duration) { + if (duration === void 0) { + duration = null; + } + + _classPrivateFieldLooseBase(this, _paused)[_paused] = true; + clearTimeout(_classPrivateFieldLooseBase(this, _pauseTimer)[_pauseTimer]); + + if (duration != null) { + _classPrivateFieldLooseBase(this, _pauseTimer)[_pauseTimer] = setTimeout(_classPrivateFieldLooseBase(this, _resume)[_resume], duration); + } + } + /** + * Pauses the queue for a duration, and lower the limit of concurrent requests + * when the queue resumes. When the queue resumes, it tries to progressively + * increase the limit in `this.#increaseLimit` until another call is made to + * `this.rateLimit`. + * Call this function when using the RateLimitedQueue for network requests and + * the remote server responds with 429 HTTP code. + * + * @param {number} duration in milliseconds. + */ + + + rateLimit(duration) { + clearTimeout(_classPrivateFieldLooseBase(this, _rateLimitingTimer)[_rateLimitingTimer]); + this.pause(duration); + + if (this.limit > 1 && Number.isFinite(this.limit)) { + _classPrivateFieldLooseBase(this, _upperLimit)[_upperLimit] = this.limit - 1; + this.limit = _classPrivateFieldLooseBase(this, _downLimit)[_downLimit]; + _classPrivateFieldLooseBase(this, _rateLimitingTimer)[_rateLimitingTimer] = setTimeout(_classPrivateFieldLooseBase(this, _increaseLimit)[_increaseLimit], duration); + } + } + + get isPaused() { + return _classPrivateFieldLooseBase(this, _paused)[_paused]; + } + } function _call2(fn) { @@ -5163,7 +5443,7 @@ function _queueNext2() { } function _next2() { - if (_classPrivateFieldLooseBase(this, _activeRequests)[_activeRequests] >= this.limit) { + if (_classPrivateFieldLooseBase(this, _paused)[_paused] || _classPrivateFieldLooseBase(this, _activeRequests)[_activeRequests] >= this.limit) { return; } @@ -5182,7 +5462,11 @@ function _next2() { next.done = handler.done; } -function _queue2(fn, options = {}) { +function _queue2(fn, options) { + if (options === void 0) { + options = {}; + } + const handler = { fn, priority: options.priority || 0, @@ -5219,7 +5503,7 @@ module.exports = { RateLimitedQueue, internalRateLimitedQueue: Symbol('__queue') }; -},{}],28:[function(require,module,exports){ +},{}],31:[function(require,module,exports){ "use strict"; var _apply; @@ -5390,7 +5674,7 @@ function _apply2(locale) { }; this.locale.pluralize = locale.pluralize || prevLocale.pluralize; } -},{"./hasProperty":43}],29:[function(require,module,exports){ +},{"./hasProperty":46}],32:[function(require,module,exports){ "use strict"; const { @@ -5437,7 +5721,7 @@ module.exports = function delay(ms, opts) { return undefined; }); }; -},{"./AbortController":23}],30:[function(require,module,exports){ +},{"./AbortController":26}],33:[function(require,module,exports){ "use strict"; const throttle = require('lodash.throttle'); @@ -5463,7 +5747,7 @@ module.exports = throttle(emitSocketProgress, 300, { leading: true, trailing: true }); -},{"lodash.throttle":50}],31:[function(require,module,exports){ +},{"lodash.throttle":54}],34:[function(require,module,exports){ "use strict"; const NetworkError = require('./NetworkError'); @@ -5472,8 +5756,8 @@ const NetworkError = require('./NetworkError'); */ -module.exports = function fetchWithNetworkError(...options) { - return fetch(...options).catch(err => { +module.exports = function fetchWithNetworkError() { + return fetch(...arguments).catch(err => { if (err.name === 'AbortError') { throw err; } else { @@ -5481,7 +5765,7 @@ module.exports = function fetchWithNetworkError(...options) { } }); }; -},{"./NetworkError":25}],32:[function(require,module,exports){ +},{"./NetworkError":28}],35:[function(require,module,exports){ "use strict"; const isDOMElement = require('./isDOMElement'); @@ -5493,7 +5777,11 @@ const isDOMElement = require('./isDOMElement'); */ -module.exports = function findDOMElement(element, context = document) { +module.exports = function findDOMElement(element, context) { + if (context === void 0) { + context = document; + } + if (typeof element === 'string') { return context.querySelector(element); } @@ -5504,7 +5792,7 @@ module.exports = function findDOMElement(element, context = document) { return null; }; -},{"./isDOMElement":44}],33:[function(require,module,exports){ +},{"./isDOMElement":47}],36:[function(require,module,exports){ "use strict"; function encodeCharacter(character) { @@ -5554,7 +5842,7 @@ module.exports = function generateFileID(file) { return id; }; -},{}],34:[function(require,module,exports){ +},{}],37:[function(require,module,exports){ "use strict"; const webkitGetAsEntryApi = require('./utils/webkitGetAsEntryApi/index'); @@ -5576,11 +5864,13 @@ const fallbackApi = require('./utils/fallbackApi'); */ -module.exports = function getDroppedFiles(dataTransfer, { - logDropError = () => {} -} = {}) { +module.exports = function getDroppedFiles(dataTransfer, _temp) { var _dataTransfer$items; + let { + logDropError = () => {} + } = _temp === void 0 ? {} : _temp; + // Get all files from all subdirs. Works (at least) in Chrome, Mozilla, and Safari if ((_dataTransfer$items = dataTransfer.items) != null && _dataTransfer$items[0] && 'webkitGetAsEntry' in dataTransfer.items[0]) { return webkitGetAsEntryApi(dataTransfer, logDropError); // Otherwise just return all first-order files @@ -5588,7 +5878,7 @@ module.exports = function getDroppedFiles(dataTransfer, { return fallbackApi(dataTransfer); }; -},{"./utils/fallbackApi":35,"./utils/webkitGetAsEntryApi/index":38}],35:[function(require,module,exports){ +},{"./utils/fallbackApi":38,"./utils/webkitGetAsEntryApi/index":41}],38:[function(require,module,exports){ "use strict"; const toArray = require('../../toArray'); // .files fallback, should be implemented in any browser @@ -5598,7 +5888,7 @@ module.exports = function fallbackApi(dataTransfer) { const files = toArray(dataTransfer.files); return Promise.resolve(files); }; -},{"../../toArray":48}],36:[function(require,module,exports){ +},{"../../toArray":51}],39:[function(require,module,exports){ "use strict"; /** @@ -5609,9 +5899,10 @@ module.exports = function fallbackApi(dataTransfer) { * @param {Function} logDropError * @param {Function} callback - called with ([ all files and directories in that directoryReader ]) */ -module.exports = function getFilesAndDirectoriesFromDirectory(directoryReader, oldEntries, logDropError, { - onSuccess -}) { +module.exports = function getFilesAndDirectoriesFromDirectory(directoryReader, oldEntries, logDropError, _ref) { + let { + onSuccess + } = _ref; directoryReader.readEntries(entries => { const newEntries = [...oldEntries, ...entries]; // According to the FileSystem API spec, getFilesAndDirectoriesFromDirectory() // must be called until it calls the onSuccess with an empty array. @@ -5631,7 +5922,7 @@ module.exports = function getFilesAndDirectoriesFromDirectory(directoryReader, o onSuccess(oldEntries); }); }; -},{}],37:[function(require,module,exports){ +},{}],40:[function(require,module,exports){ "use strict"; /** @@ -5652,7 +5943,7 @@ module.exports = function getRelativePath(fileEntry) { return fileEntry.fullPath; }; -},{}],38:[function(require,module,exports){ +},{}],41:[function(require,module,exports){ "use strict"; const toArray = require('../../../toArray'); @@ -5703,7 +5994,7 @@ module.exports = function webkitGetAsEntryApi(dataTransfer, logDropError) { }); return Promise.all(rootPromises).then(() => files); }; -},{"../../../toArray":48,"./getFilesAndDirectoriesFromDirectory":36,"./getRelativePath":37}],39:[function(require,module,exports){ +},{"../../../toArray":51,"./getFilesAndDirectoriesFromDirectory":39,"./getRelativePath":40}],42:[function(require,module,exports){ "use strict"; /** @@ -5727,7 +6018,7 @@ module.exports = function getFileNameAndExtension(fullFileName) { extension: fullFileName.slice(lastDot + 1) }; }; -},{}],40:[function(require,module,exports){ +},{}],43:[function(require,module,exports){ "use strict"; const getFileNameAndExtension = require('./getFileNameAndExtension'); @@ -5748,7 +6039,7 @@ module.exports = function getFileType(file) { return 'application/octet-stream'; }; -},{"./getFileNameAndExtension":39,"./mimeTypes":46}],41:[function(require,module,exports){ +},{"./getFileNameAndExtension":42,"./mimeTypes":49}],44:[function(require,module,exports){ "use strict"; module.exports = function getSocketHost(url) { @@ -5758,7 +6049,7 @@ module.exports = function getSocketHost(url) { const socketProtocol = /^http:\/\//i.test(url) ? 'ws' : 'wss'; return `${socketProtocol}://${host}`; }; -},{}],42:[function(require,module,exports){ +},{}],45:[function(require,module,exports){ "use strict"; /** @@ -5782,13 +6073,13 @@ module.exports = function getTimeStamp() { const seconds = pad(date.getSeconds()); return `${hours}:${minutes}:${seconds}`; }; -},{}],43:[function(require,module,exports){ +},{}],46:[function(require,module,exports){ "use strict"; module.exports = function has(object, key) { return Object.prototype.hasOwnProperty.call(object, key); }; -},{}],44:[function(require,module,exports){ +},{}],47:[function(require,module,exports){ "use strict"; /** @@ -5799,7 +6090,7 @@ module.exports = function has(object, key) { module.exports = function isDOMElement(obj) { return (obj == null ? void 0 : obj.nodeType) === Node.ELEMENT_NODE; }; -},{}],45:[function(require,module,exports){ +},{}],48:[function(require,module,exports){ "use strict"; function isNetworkError(xhr) { @@ -5811,7 +6102,7 @@ function isNetworkError(xhr) { } module.exports = isNetworkError; -},{}],46:[function(require,module,exports){ +},{}],49:[function(require,module,exports){ "use strict"; // ___Why not add the mime-types package? @@ -5869,7 +6160,7 @@ module.exports = { gz: 'application/gzip', dmg: 'application/x-apple-diskimage' }; -},{}],47:[function(require,module,exports){ +},{}],50:[function(require,module,exports){ "use strict"; module.exports = function settle(promises) { @@ -5892,14 +6183,14 @@ module.exports = function settle(promises) { }; }); }; -},{}],48:[function(require,module,exports){ +},{}],51:[function(require,module,exports){ "use strict"; /** * Converts list into array */ module.exports = Array.from; -},{}],49:[function(require,module,exports){ +},{}],52:[function(require,module,exports){ "use strict"; var _class, _temp; @@ -5908,7 +6199,7 @@ const BasePlugin = require('@uppy/core/lib/BasePlugin'); const { nanoid -} = require('nanoid'); +} = require('nanoid/non-secure'); const { Provider, @@ -5935,6 +6226,8 @@ const NetworkError = require('@uppy/utils/lib/NetworkError'); const isNetworkError = require('@uppy/utils/lib/isNetworkError'); +const locale = require('./locale'); + function buildResponseError(xhr, err) { let error = err; // No error message @@ -5978,11 +6271,7 @@ module.exports = (_temp = _class = class XHRUpload extends BasePlugin { this.type = 'uploader'; this.id = this.opts.id || 'XHRUpload'; this.title = 'XHRUpload'; - this.defaultLocale = { - strings: { - timedOut: 'Upload stalled for %{seconds} seconds, aborting.' - } - }; // Default options + this.defaultLocale = locale; // Default options const defaultOptions = { formData: true, @@ -6262,6 +6551,7 @@ module.exports = (_temp = _class = class XHRUpload extends BasePlugin { uploadRemote(file) { const opts = this.getOptions(file); return new Promise((resolve, reject) => { + this.uppy.emit('upload-started', file); const fields = {}; const metaFields = Array.isArray(opts.metaFields) ? opts.metaFields // Send along all fields by default. : Object.keys(file.meta); @@ -6289,12 +6579,12 @@ module.exports = (_temp = _class = class XHRUpload extends BasePlugin { }); this.uploaderEvents[file.id] = new EventTracker(this.uppy); this.onFileRemove(file.id, () => { - socket.send('pause', {}); + socket.send('cancel', {}); queuedRequest.abort(); resolve(`upload ${file.id} was removed`); }); this.onCancelAll(file.id, () => { - socket.send('pause', {}); + socket.send('cancel', {}); queuedRequest.abort(); resolve(`upload ${file.id} was canceled`); }); @@ -6557,8 +6847,17 @@ module.exports = (_temp = _class = class XHRUpload extends BasePlugin { this.uppy.removeUploader(this.handleUpload); } -}, _class.VERSION = "2.0.5", _temp); -},{"@uppy/companion-client":12,"@uppy/core/lib/BasePlugin":14,"@uppy/utils/lib/EventTracker":24,"@uppy/utils/lib/NetworkError":25,"@uppy/utils/lib/ProgressTimeout":26,"@uppy/utils/lib/RateLimitedQueue":27,"@uppy/utils/lib/emitSocketProgress":30,"@uppy/utils/lib/getSocketHost":41,"@uppy/utils/lib/isNetworkError":45,"@uppy/utils/lib/settle":47,"nanoid":53}],50:[function(require,module,exports){ +}, _class.VERSION = "2.0.7", _temp); +},{"./locale":53,"@uppy/companion-client":13,"@uppy/core/lib/BasePlugin":15,"@uppy/utils/lib/EventTracker":27,"@uppy/utils/lib/NetworkError":28,"@uppy/utils/lib/ProgressTimeout":29,"@uppy/utils/lib/RateLimitedQueue":30,"@uppy/utils/lib/emitSocketProgress":33,"@uppy/utils/lib/getSocketHost":44,"@uppy/utils/lib/isNetworkError":48,"@uppy/utils/lib/settle":50,"nanoid/non-secure":57}],53:[function(require,module,exports){ +"use strict"; + +module.exports = { + strings: { + // Shown in the Informer if an upload is being canceled because it stalled for too long. + timedOut: 'Upload stalled for %{seconds} seconds, aborting.' + } +}; +},{}],54:[function(require,module,exports){ (function (global){(function (){ /** * lodash (Custom Build) @@ -7001,7 +7300,7 @@ function toNumber(value) { module.exports = throttle; }).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}],51:[function(require,module,exports){ +},{}],55:[function(require,module,exports){ var wildcard = require('wildcard'); var reMimePartSplit = /[\/\+\.]/; @@ -7027,7 +7326,7 @@ module.exports = function(target, pattern) { return pattern ? test(pattern.split(';')[0]) : test; }; -},{"wildcard":57}],52:[function(require,module,exports){ +},{"wildcard":59}],56:[function(require,module,exports){ /** * Create an event emitter with namespaces * @name createNamespaceEmitter @@ -7165,313 +7464,43 @@ module.exports = function createNamespaceEmitter () { return emitter } -},{}],53:[function(require,module,exports){ -(function (process){(function (){ -// This file replaces `index.js` in bundlers like webpack or Rollup, -// according to `browser` config in `package.json`. - -let { urlAlphabet } = require('./url-alphabet/index.cjs') - -if (process.env.NODE_ENV !== 'production') { - // All bundlers will remove this block in the production bundle. - if ( - typeof navigator !== 'undefined' && - navigator.product === 'ReactNative' && - typeof crypto === 'undefined' - ) { - throw new Error( - 'React Native does not have a built-in secure random generator. ' + - 'If you don’t need unpredictable IDs use `nanoid/non-secure`. ' + - 'For secure IDs, import `react-native-get-random-values` ' + - 'before Nano ID.' - ) - } - if (typeof msCrypto !== 'undefined' && typeof crypto === 'undefined') { - throw new Error( - 'Import file with `if (!window.crypto) window.crypto = window.msCrypto`' + - ' before importing Nano ID to fix IE 11 support' - ) - } - if (typeof crypto === 'undefined') { - throw new Error( - 'Your browser does not have secure random generator. ' + - 'If you don’t need unpredictable IDs, you can use nanoid/non-secure.' - ) - } -} - -let random = bytes => crypto.getRandomValues(new Uint8Array(bytes)) - -let customRandom = (alphabet, size, getRandom) => { - // First, a bitmask is necessary to generate the ID. The bitmask makes bytes - // values closer to the alphabet size. The bitmask calculates the closest - // `2^31 - 1` number, which exceeds the alphabet size. - // For example, the bitmask for the alphabet size 30 is 31 (00011111). - // `Math.clz32` is not used, because it is not available in browsers. - let mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1 - // Though, the bitmask solution is not perfect since the bytes exceeding - // the alphabet size are refused. Therefore, to reliably generate the ID, - // the random bytes redundancy has to be satisfied. - - // Note: every hardware random generator call is performance expensive, - // because the system call for entropy collection takes a lot of time. - // So, to avoid additional system calls, extra bytes are requested in advance. - - // Next, a step determines how many random bytes to generate. - // The number of random bytes gets decided upon the ID size, mask, - // alphabet size, and magic number 1.6 (using 1.6 peaks at performance - // according to benchmarks). - - // `-~f => Math.ceil(f)` if f is a float - // `-~i => i + 1` if i is an integer - let step = -~((1.6 * mask * size) / alphabet.length) - - return () => { - let id = '' - while (true) { - let bytes = getRandom(step) - // A compact alternative for `for (var i = 0; i < step; i++)`. - let j = step - while (j--) { - // Adding `|| ''` refuses a random byte that exceeds the alphabet size. - id += alphabet[bytes[j] & mask] || '' - if (id.length === size) return id - } - } - } -} - -let customAlphabet = (alphabet, size) => customRandom(alphabet, size, random) - -let nanoid = (size = 21) => { - let id = '' - let bytes = crypto.getRandomValues(new Uint8Array(size)) - - // A compact alternative for `for (var i = 0; i < step; i++)`. - while (size--) { - // It is incorrect to use bytes exceeding the alphabet size. - // The following mask reduces the random byte in the 0-255 value - // range to the 0-63 value range. Therefore, adding hacks, such - // as empty string fallback or magic numbers, is unneccessary because - // the bitmask trims bytes down to the alphabet size. - let byte = bytes[size] & 63 - if (byte < 36) { - // `0-9a-z` - id += byte.toString(36) - } else if (byte < 62) { - // `A-Z` - id += (byte - 26).toString(36).toUpperCase() - } else if (byte < 63) { - id += '_' - } else { - id += '-' - } - } - return id -} - -module.exports = { nanoid, customAlphabet, customRandom, urlAlphabet, random } - -}).call(this)}).call(this,require('_process')) -},{"./url-alphabet/index.cjs":54,"_process":56}],54:[function(require,module,exports){ +},{}],57:[function(require,module,exports){ // This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped // optimize the gzip compression for this alphabet. let urlAlphabet = 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW' -module.exports = { urlAlphabet } +let customAlphabet = (alphabet, size) => { + return () => { + let id = '' + // A compact alternative for `for (var i = 0; i < step; i++)`. + let i = size + while (i--) { + // `| 0` is more compact and faster than `Math.floor()`. + id += alphabet[(Math.random() * alphabet.length) | 0] + } + return id + } +} -},{}],55:[function(require,module,exports){ +let nanoid = (size = 21) => { + let id = '' + // A compact alternative for `for (var i = 0; i < step; i++)`. + let i = size + while (i--) { + // `| 0` is more compact and faster than `Math.floor()`. + id += urlAlphabet[(Math.random() * 64) | 0] + } + return id +} + +module.exports = { nanoid, customAlphabet } + +},{}],58:[function(require,module,exports){ var n,l,u,t,i,o,r,f,e={},c=[],s=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function a(n,l){for(var u in l)n[u]=l[u];return n}function p(n){var l=n.parentNode;l&&l.removeChild(n)}function v(l,u,t){var i,o,r,f={};for(r in u)"key"==r?i=u[r]:"ref"==r?o=u[r]:f[r]=u[r];if(arguments.length>2&&(f.children=arguments.length>3?n.call(arguments,2):t),"function"==typeof l&&null!=l.defaultProps)for(r in l.defaultProps)void 0===f[r]&&(f[r]=l.defaultProps[r]);return h(l,f,i,o,null)}function h(n,t,i,o,r){var f={type:n,props:t,key:i,ref:o,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==r?++u:r};return null!=l.vnode&&l.vnode(f),f}function y(n){return n.children}function d(n,l){this.props=n,this.context=l}function _(n,l){if(null==l)return n.__?_(n.__,n.__.__k.indexOf(n)+1):null;for(var u;l0?h(k.type,k.props,k.key,null,k.__v):k)){if(k.__=u,k.__b=u.__b+1,null===(d=A[p])||d&&k.key==d.key&&k.type===d.type)A[p]=void 0;else for(v=0;v2&&(f.children=arguments.length>3?n.call(arguments,2):t),h(l.type,f,i||l.key,o||l.ref,null)},exports.createContext=function(n,l){var u={__c:l="__cC"+f++,__:n,Consumer:function(n,l){return n.children(l)},Provider:function(n){var u,t;return this.getChildContext||(u=[],(t={})[l]=this,this.getChildContext=function(){return t},this.shouldComponentUpdate=function(n){this.props.value!==n.value&&u.some(x)},this.sub=function(n){u.push(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){u.splice(u.indexOf(n),1),l&&l.call(n)}}),n.children}};return u.Provider.__=u.Consumer.contextType=u},exports.toChildArray=function n(l,u){return u=u||[],null==l||"boolean"==typeof l||(Array.isArray(l)?l.some(function(l){n(l,u)}):u.push(l)),u},exports.options=l; -},{}],56:[function(require,module,exports){ -// shim for using process in browser -var process = module.exports = {}; - -// cached from whatever global is present so that test runners that stub it -// don't break things. But we need to wrap it in a try catch in case it is -// wrapped in strict mode code which doesn't define any globals. It's inside a -// function because try/catches deoptimize in certain engines. - -var cachedSetTimeout; -var cachedClearTimeout; - -function defaultSetTimout() { - throw new Error('setTimeout has not been defined'); -} -function defaultClearTimeout () { - throw new Error('clearTimeout has not been defined'); -} -(function () { - try { - if (typeof setTimeout === 'function') { - cachedSetTimeout = setTimeout; - } else { - cachedSetTimeout = defaultSetTimout; - } - } catch (e) { - cachedSetTimeout = defaultSetTimout; - } - try { - if (typeof clearTimeout === 'function') { - cachedClearTimeout = clearTimeout; - } else { - cachedClearTimeout = defaultClearTimeout; - } - } catch (e) { - cachedClearTimeout = defaultClearTimeout; - } -} ()) -function runTimeout(fun) { - if (cachedSetTimeout === setTimeout) { - //normal enviroments in sane situations - return setTimeout(fun, 0); - } - // if setTimeout wasn't available but was latter defined - if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { - cachedSetTimeout = setTimeout; - return setTimeout(fun, 0); - } - try { - // when when somebody has screwed with setTimeout but no I.E. maddness - return cachedSetTimeout(fun, 0); - } catch(e){ - try { - // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally - return cachedSetTimeout.call(null, fun, 0); - } catch(e){ - // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error - return cachedSetTimeout.call(this, fun, 0); - } - } - - -} -function runClearTimeout(marker) { - if (cachedClearTimeout === clearTimeout) { - //normal enviroments in sane situations - return clearTimeout(marker); - } - // if clearTimeout wasn't available but was latter defined - if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { - cachedClearTimeout = clearTimeout; - return clearTimeout(marker); - } - try { - // when when somebody has screwed with setTimeout but no I.E. maddness - return cachedClearTimeout(marker); - } catch (e){ - try { - // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally - return cachedClearTimeout.call(null, marker); - } catch (e){ - // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. - // Some versions of I.E. have different rules for clearTimeout vs setTimeout - return cachedClearTimeout.call(this, marker); - } - } - - - -} -var queue = []; -var draining = false; -var currentQueue; -var queueIndex = -1; - -function cleanUpNextTick() { - if (!draining || !currentQueue) { - return; - } - draining = false; - if (currentQueue.length) { - queue = currentQueue.concat(queue); - } else { - queueIndex = -1; - } - if (queue.length) { - drainQueue(); - } -} - -function drainQueue() { - if (draining) { - return; - } - var timeout = runTimeout(cleanUpNextTick); - draining = true; - - var len = queue.length; - while(len) { - currentQueue = queue; - queue = []; - while (++queueIndex < len) { - if (currentQueue) { - currentQueue[queueIndex].run(); - } - } - queueIndex = -1; - len = queue.length; - } - currentQueue = null; - draining = false; - runClearTimeout(timeout); -} - -process.nextTick = function (fun) { - var args = new Array(arguments.length - 1); - if (arguments.length > 1) { - for (var i = 1; i < arguments.length; i++) { - args[i - 1] = arguments[i]; - } - } - queue.push(new Item(fun, args)); - if (queue.length === 1 && !draining) { - runTimeout(drainQueue); - } -}; - -// v8 likes predictible objects -function Item(fun, array) { - this.fun = fun; - this.array = array; -} -Item.prototype.run = function () { - this.fun.apply(null, this.array); -}; -process.title = 'browser'; -process.browser = true; -process.env = {}; -process.argv = []; -process.version = ''; // empty string to avoid regexp issues -process.versions = {}; - -function noop() {} - -process.on = noop; -process.addListener = noop; -process.once = noop; -process.off = noop; -process.removeListener = noop; -process.removeAllListeners = noop; -process.emit = noop; -process.prependListener = noop; -process.prependOnceListener = noop; - -process.listeners = function (name) { return [] } - -process.binding = function (name) { - throw new Error('process.binding is not supported'); -}; - -process.cwd = function () { return '/' }; -process.chdir = function (dir) { - throw new Error('process.chdir is not supported'); -}; -process.umask = function() { return 0; }; - -},{}],57:[function(require,module,exports){ +},{}],59:[function(require,module,exports){ /* jshint node: true */ 'use strict'; @@ -7566,7 +7595,7 @@ module.exports = function(text, test, separator) { return matcher; }; -},{}],58:[function(require,module,exports){ +},{}],60:[function(require,module,exports){ // We need a custom build of Uppy because we do not use webpack for // our JS modules/build. The only way to get what you want from Uppy // is to use the webpack modules or to include the entire Uppy project @@ -7584,4 +7613,4 @@ Uppy.Utils = { AbortControllerLib: require('@uppy/utils/lib/AbortController') } -},{"@uppy/aws-s3":5,"@uppy/aws-s3-multipart":3,"@uppy/core":18,"@uppy/drop-target":21,"@uppy/utils/lib/AbortController":23,"@uppy/utils/lib/EventTracker":24,"@uppy/utils/lib/delay":29,"@uppy/xhr-upload":49}]},{},[58]); +},{"@uppy/aws-s3":5,"@uppy/aws-s3-multipart":3,"@uppy/core":20,"@uppy/drop-target":24,"@uppy/utils/lib/AbortController":26,"@uppy/utils/lib/EventTracker":27,"@uppy/utils/lib/delay":32,"@uppy/xhr-upload":52}]},{},[60]); diff --git a/yarn.lock b/yarn.lock index 18747c95af..124b99d259 100644 --- a/yarn.lock +++ b/yarn.lock @@ -270,72 +270,72 @@ dependencies: "@types/node" "*" -"@uppy/aws-s3-multipart@^2.1.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@uppy/aws-s3-multipart/-/aws-s3-multipart-2.1.1.tgz#7749491067ab72249dab201cc12409e57f2dbb1a" - integrity sha512-p+oFSCWEUc7ptv73sdZuWoq10hh0vzmP4cxwBEX/+nrplLFSuRUJ+z2XnNEigo8jXHWbA86k6tEX/3XIUsslgg== +"@uppy/aws-s3-multipart@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@uppy/aws-s3-multipart/-/aws-s3-multipart-2.2.1.tgz#385705b3ae6b56abee28fb086c046eeaeceee62a" + integrity sha512-57MZw2hxcBVeXp7xxdPNna+7HMckiJsrq/vwNM74aforLpNNSYE1B3JsiBeXU7fvIWx/W+5udtl8aAIKQxpJqw== dependencies: - "@uppy/companion-client" "^2.0.3" - "@uppy/utils" "^4.0.3" + "@uppy/companion-client" "^2.0.5" + "@uppy/utils" "^4.0.5" -"@uppy/aws-s3@^2.0.4": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@uppy/aws-s3/-/aws-s3-2.0.5.tgz#dae2edb819b8e79119304a1659b931a862bf1e45" - integrity sha512-VWqVtmKtV/wSLCZdFbWlUt+CS7W/KZv20Pmm3JgcDLrQk3PdciYg3L9x65FTP8kSDsiXCwMg7uO5HfbspZWx9Q== +"@uppy/aws-s3@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@uppy/aws-s3/-/aws-s3-2.0.8.tgz#fd82b7db4d54ed118d369f856efe3d849e0482e3" + integrity sha512-ihZF3SpXZCZPxNapXshBhSC4TwNv0JlASZqd6T+u48Ojb6FZbYs7BgXLnLQooOGZPUx1UXtJREBh9SjXvn1lWw== dependencies: - "@uppy/companion-client" "^2.0.3" - "@uppy/utils" "^4.0.3" - "@uppy/xhr-upload" "^2.0.5" + "@uppy/companion-client" "^2.0.5" + "@uppy/utils" "^4.0.5" + "@uppy/xhr-upload" "^2.0.7" nanoid "^3.1.25" -"@uppy/companion-client@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-2.0.3.tgz#d3cd30ebbc9f87d27374d13258b5d304366f10d5" - integrity sha512-I1baKKBpb3d//q3agRtNV3UD/sA7EecFOfoVSpMlPkFu6oQqxjSC5OFXTf3fa8X+wo4Lcutv1++3igPJ1zrgbA== +"@uppy/companion-client@^2.0.4", "@uppy/companion-client@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-2.0.5.tgz#ee0c177dac22afab132369f467d82e7017eeb468" + integrity sha512-yAeYbpQ+yHcklKVbkRy83V1Zv/0kvaTDTHaBvaaPmLtcKgeZE3pUjEI/7v2sTxvCVSy4cRjd9TRSXSSl5UCnuQ== dependencies: - "@uppy/utils" "^4.0.3" + "@uppy/utils" "^4.0.5" namespace-emitter "^2.0.1" -"@uppy/core@^2.1.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@uppy/core/-/core-2.1.1.tgz#503b3172ffe32e6cc7385f5b0c99f008ade815f1" - integrity sha512-dFlcy6+05zwsJk1KNeUKVWUyAfhOVwNpnPLaR1NX9Qsjv7KlYfUNRVW3uCCmIpd/EZsX44+haiqGrhLcYDAcxA== +"@uppy/core@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@uppy/core/-/core-2.1.6.tgz#8e3c6eca12c91118a6340a1aedc777c6b4f2f6f5" + integrity sha512-WTGthAAHMfB6uAtISbu+7jYh4opnBWHSf7A0jsPdREwXc4hrhC/z9lbejZfSLkVDXdbNwpWWH38EgOGCNQb5MQ== dependencies: "@transloadit/prettier-bytes" "0.0.7" - "@uppy/store-default" "^2.0.2" - "@uppy/utils" "^4.0.3" + "@uppy/store-default" "^2.0.3" + "@uppy/utils" "^4.0.5" lodash.throttle "^4.1.1" mime-match "^1.0.2" namespace-emitter "^2.0.1" nanoid "^3.1.25" preact "^10.5.13" -"@uppy/drop-target@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@uppy/drop-target/-/drop-target-1.1.1.tgz#9bfbcb7b284ef605d01fc24823f857cbad51377a" - integrity sha512-2MxNGEkI2vt1D6MEa0PNqR+VTMbuUzmiytHyy57phZNCNes8K4BdnneBwla2nG3LI0D1TURK7MKxaSjv93d3Vg== +"@uppy/drop-target@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@uppy/drop-target/-/drop-target-1.1.2.tgz#a78cc1c1947e55be4e16de71efdbacf9dfb7effd" + integrity sha512-iyLckwpxDqZr7ysH94cWwgta9P9SFus0sayXb9Lr/Kd0lk+tK/bcrJmsJHp4HYDFW7C8RphYdUu78C8NNXL09w== dependencies: - "@uppy/utils" "^4.0.3" + "@uppy/utils" "^4.0.5" -"@uppy/store-default@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-2.0.2.tgz#c0464e92452fdc7d4cd1548d2c7453017cad7a98" - integrity sha512-D9oz08EYBoc4fDotvaevd2Q7uVldS61HYFOXK20b5M/xXF/uxepapaqQnMu1DfCVsA77rhp7DMemxnWc9y8xTQ== +"@uppy/store-default@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-2.0.3.tgz#47ad4fc4816d21955ff37d6bb5096a93278c29e2" + integrity sha512-2BGlN1sW0cFv4rOqTK8dfSg579S984N1HxCJxLFqeW9nWD6zd/O8Omyd85tbxGQ+FLZLTmLOm/feD0YeCBMahg== -"@uppy/utils@^4.0.3": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-4.0.3.tgz#181fdd161e1450d31af0cf7bc97946a99196a8fe" - integrity sha512-LApneC8lNvTonzSJFupxzuEvKhwp/Klc1otq8t+zXpdgjLVVSuW/rJBFfdIDrmDoqSzVLQKYjMy07CmhDAWfKg== +"@uppy/utils@^4.0.4", "@uppy/utils@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-4.0.5.tgz#0feda6e03d13af2fec969b146d7410462a8d2d48" + integrity sha512-uRv921A69UMjuWCLSC5tKXuIVoMOROVpFstIAQv5CoiCOCXyofcWpvAqELT7qlQJ5VRWha3uF5d/Z94SNnwxew== dependencies: lodash.throttle "^4.1.1" -"@uppy/xhr-upload@^2.0.4", "@uppy/xhr-upload@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@uppy/xhr-upload/-/xhr-upload-2.0.5.tgz#5792a7ff0bfb1503c8a9cccefb48ddb40deb11de" - integrity sha512-DkD6cRKrcI4oDmCimHAULb6rruyUt6SbH4/omhpvWILbG/mWV5vA39YLvYxCZ1FZbijJ4QkVTKEeOTLcmoljPg== +"@uppy/xhr-upload@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@uppy/xhr-upload/-/xhr-upload-2.0.7.tgz#d9128be1fdde78edc61878e23930a2855f607df5" + integrity sha512-bzCc654B0HfNmL4BIr7gGTvg2pQBucYgPmAb4ST7jGyWlEJWbSxMXR/19zvISQzpJ6v1uP6q2ppgxGMqNdj/rA== dependencies: - "@uppy/companion-client" "^2.0.3" - "@uppy/utils" "^4.0.3" + "@uppy/companion-client" "^2.0.4" + "@uppy/utils" "^4.0.4" nanoid "^3.1.25" JSONStream@^1.0.3: From cbaf7c949bdb720149da9a8d5dfe34f58a837495 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Wed, 23 Mar 2022 17:36:08 +0200 Subject: [PATCH 008/195] FIX: Make sure max_oneboxes_per_post is enforced (#16215) PostAnalyzer and CookedPostProcessor both replace URLs with oneboxes. PostAnalyzer did not use the max_oneboxes_per_post site and setting and CookedPostProcessor replaced at most max_oneboxes_per_post URLs ignoring the oneboxes that were replaced already by PostAnalyzer. --- app/models/post_analyzer.rb | 4 ++++ lib/cooked_processor_mixin.rb | 2 +- spec/lib/cooked_post_processor_spec.rb | 18 ++++++++++++++++-- spec/models/post_analyzer_spec.rb | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index bd8d58e79b..4030df5d59 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -31,7 +31,11 @@ class PostAnalyzer cooked = PrettyText.cook(raw, opts) end + limit = SiteSetting.max_oneboxes_per_post result = Oneboxer.apply(cooked) do |url| + next if limit <= 0 + limit -= 1 + @onebox_urls << url if opts[:invalidate_oneboxes] Oneboxer.invalidate(url) diff --git a/lib/cooked_processor_mixin.rb b/lib/cooked_processor_mixin.rb index 7c6c8ed1fa..9985c1684b 100644 --- a/lib/cooked_processor_mixin.rb +++ b/lib/cooked_processor_mixin.rb @@ -3,7 +3,7 @@ module CookedProcessorMixin def post_process_oneboxes - limit = SiteSetting.max_oneboxes_per_post + limit = SiteSetting.max_oneboxes_per_post - @doc.css("aside.onebox, a.inline-onebox").size oneboxes = {} inlineOneboxes = {} diff --git a/spec/lib/cooked_post_processor_spec.rb b/spec/lib/cooked_post_processor_spec.rb index 72e96274e3..7800930a8c 100644 --- a/spec/lib/cooked_post_processor_spec.rb +++ b/spec/lib/cooked_post_processor_spec.rb @@ -56,6 +56,20 @@ describe CookedPostProcessor do before do SiteSetting.enable_inline_onebox_on_all_domains = true + Oneboxer.stubs(:cached_onebox).with(url).returns <<~HTML + + HTML + Oneboxer.stubs(:cached_onebox).with(not_oneboxed_url).returns(nil) %i{head get}.each do |method| stub_request(method, url).to_return( @@ -90,11 +104,11 @@ describe CookedPostProcessor do count: 2 ) - expect(cpp.html).to have_tag('aside.onebox a', text: title, count: 2) + expect(cpp.html).to have_tag('aside.onebox a', text: title, count: 1) expect(cpp.html).to have_tag('aside.onebox a', text: url_hostname, - count: 2 + count: 1 ) expect(cpp.html).to have_tag('a', diff --git a/spec/models/post_analyzer_spec.rb b/spec/models/post_analyzer_spec.rb index c0813d5343..b82f8d0d4c 100644 --- a/spec/models/post_analyzer_spec.rb +++ b/spec/models/post_analyzer_spec.rb @@ -58,6 +58,25 @@ describe PostAnalyzer do cooked = post_analyzer.cook('*this is italic*') expect(cooked).to eq('

this is italic

') end + + it 'should respect SiteSetting.max_oneboxes_per_post' do + SiteSetting.max_oneboxes_per_post = 2 + Oneboxer.expects(:cached_onebox).with(url).returns('something').twice + + cooked = post_analyzer.cook(<<~RAW) + #{url} + + #{url} + + #{url} + RAW + + expect(cooked).to match_html(<<~HTML) +

something

+

something

+

#{url}

+ HTML + end end context "links" do From 4a39850aacbc59daa1a771fadb6701ab9ffba372 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 23 Mar 2022 16:42:53 +0100 Subject: [PATCH 009/195] FIX: closing the picker shouldn't propagate the pointer event (#16266) --- .../discourse/app/components/emoji-picker.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker.js b/app/assets/javascripts/discourse/app/components/emoji-picker.js index 9d577d8b83..d54f52160e 100644 --- a/app/assets/javascripts/discourse/app/components/emoji-picker.js +++ b/app/assets/javascripts/discourse/app/components/emoji-picker.js @@ -153,9 +153,10 @@ export default Component.extend({ }, @action - onClose() { + onClose(event) { + event?.stopPropagation(); document.removeEventListener("click", this.handleOutsideClick); - this.onEmojiPickerClose && this.onEmojiPickerClose(); + this.onEmojiPickerClose && this.onEmojiPickerClose(event); }, diversityScales: computed("selectedDiversity", function () { @@ -221,7 +222,7 @@ export default Component.extend({ }); if (this.site.isMobileDevice) { - this.onClose(); + this.onClose(event); } }, @@ -236,7 +237,7 @@ export default Component.extend({ @action keydown(event) { if (event.code === "Escape") { - this.onClose(); + this.onClose(event); return false; } }, @@ -334,7 +335,7 @@ export default Component.extend({ handleOutsideClick(event) { const emojiPicker = document.querySelector(".emoji-picker"); if (emojiPicker && !emojiPicker.contains(event.target)) { - this.onClose(); + this.onClose(event); } }, }); From 99a6f32554e43a15da491c0e4db28a3537e98358 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Wed, 23 Mar 2022 13:34:17 -0400 Subject: [PATCH 010/195] DEV: Add `registerCustomLastUnreadUrlCallback`to plugin API (#16222) --- .../discourse/app/lib/plugin-api.js | 18 ++++++- .../javascripts/discourse/app/models/topic.js | 23 ++++++++ .../acceptance/topic-list-plugin-api-test.js | 29 ++++++++++ docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md | 53 ++++++++++++------- 4 files changed, 102 insertions(+), 21 deletions(-) create mode 100644 app/assets/javascripts/discourse/tests/acceptance/topic-list-plugin-api-test.js diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 8375b4f337..74aff2737d 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -84,6 +84,7 @@ import { registerTopicFooterDropdown } from "discourse/lib/register-topic-footer import { registerDesktopNotificationHandler } from "discourse/lib/desktop-notifications"; import { replaceFormatter } from "discourse/lib/utilities"; import { replaceTagRenderer } from "discourse/lib/render-tag"; +import { registerCustomLastUnreadUrlCallback } from "discourse/models/topic"; import { setNewCategoryDefaultColors } from "discourse/routes/new-category"; import { addSearchResultsCallback } from "discourse/lib/search"; import { @@ -98,7 +99,7 @@ import { consolePrefix } from "discourse/lib/source-identifier"; // based on Semantic Versioning 2.0.0. Please update the changelog at // docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version // using the format described at https://keepachangelog.com/en/1.0.0/. -const PLUGIN_API_VERSION = "1.1.0"; +const PLUGIN_API_VERSION = "1.2.0"; // This helper prevents us from applying the same `modifyClass` over and over in test mode. function canModify(klass, type, resolverName, changes) { @@ -1290,6 +1291,21 @@ class PluginApi { replaceTagRenderer(fn); } + /** + * Register a custom last unread url for a topic list item. + * If a non-null value is returned, it will be used right away. + * + * Example: + * + * function testLastUnreadUrl(context) { + * return context.urlForPostNumber(1); + * } + * api.registerCustomLastUnreadUrlCallback(testLastUnreadUrl); + **/ + registerCustomLastUnreadUrlCallback(fn) { + registerCustomLastUnreadUrlCallback(fn); + } + /** * Registers custom languages for use with HighlightJS. * diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js index ae74d2ea63..6cf798087e 100644 --- a/app/assets/javascripts/discourse/app/models/topic.js +++ b/app/assets/javascripts/discourse/app/models/topic.js @@ -41,6 +41,7 @@ export function loadTopicView(topic, args) { } export const ID_CONSTRAINT = /^\d+$/; +let _customLastUnreadUrlCallbacks = []; const Topic = RestModel.extend({ message: null, @@ -256,6 +257,19 @@ const Topic = RestModel.extend({ @discourseComputed("last_read_post_number", "highest_post_number", "url") lastUnreadUrl(lastReadPostNumber, highestPostNumber) { + let customUrl = null; + _customLastUnreadUrlCallbacks.some((cb) => { + const result = cb(this); + if (result) { + customUrl = result; + return true; + } + }); + + if (customUrl) { + return customUrl; + } + if (highestPostNumber <= lastReadPostNumber) { if (this.get("category.navigate_to_first_post_after_read")) { return this.urlForPostNumber(1); @@ -876,4 +890,13 @@ export function mergeTopic(topicId, data) { ); } +export function registerCustomLastUnreadUrlCallback(fn) { + _customLastUnreadUrlCallbacks.push(fn); +} + +// Should only be used in tests +export function clearCustomLastUnreadUrlCallbacks() { + _customLastUnreadUrlCallbacks.clear(); +} + export default Topic; diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-list-plugin-api-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-list-plugin-api-test.js new file mode 100644 index 0000000000..77f92ba29b --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-list-plugin-api-test.js @@ -0,0 +1,29 @@ +import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; +import { clearCustomLastUnreadUrlCallbacks } from "discourse/models/topic"; +import { test } from "qunit"; +import { visit } from "@ember/test-helpers"; +import { withPluginApi } from "discourse/lib/plugin-api"; + +acceptance("Topic list plugin API", function () { + function customLastUnreadUrl(context) { + return `${context.urlForPostNumber(1)}?overriden`; + } + + test("Overrides lastUnreadUrl", async function (assert) { + try { + withPluginApi("1.2.0", (api) => { + api.registerCustomLastUnreadUrlCallback(customLastUnreadUrl); + }); + + await visit("/"); + assert.strictEqual( + query( + ".topic-list .topic-list-item:first-child a.raw-topic-link" + ).getAttribute("href"), + "/t/error-after-upgrade-to-0-9-7-9/11557/1?overriden" + ); + } finally { + clearCustomLastUnreadUrlCallbacks(); + } + }); +}); diff --git a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md index 8f0e699a4f..0a380fbe82 100644 --- a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md +++ b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md @@ -2,36 +2,49 @@ All notable changes to the Discourse JavaScript plugin API located at app/assets/javascripts/discourse/app/lib/plugin-api.js will be described -in this file.. +in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.1.0] - 2021-12-15 +## [1.2.0] - 2022-03-18 + ### Added + +- Adds `registerCustomLastUnreadUrlCallback`, which allows users to register a custom + function that returns a last unread url for a topic list item. When multiple callbacks + are registered, the first non-null value that is returned will be used. + +## [1.1.0] - 2021-12-15 + +### Added + - Adds `addPosterIcons`, which allows users to add multiple icons to a poster. The -addition of this function also makes the existing `addPosterIcon` now an alias to this -function. Users may now just use `addPosterIcons` for both one or many icons. This -function allows users to now return many icons depending on an `attrs`. + addition of this function also makes the existing `addPosterIcon` now an alias to this + function. Users may now just use `addPosterIcons` for both one or many icons. This + function allows users to now return many icons depending on an `attrs`. ## [1.0.0] - 2021-11-25 + ### Removed + - Removes the `addComposerUploadProcessor` function, which is no longer used in -favour of `addComposerUploadPreProcessor`. The former was used to add preprocessors -for client side uploads via jQuery file uploader (described at -https://github.com/blueimp/jQuery-File-Upload/wiki/Options#file-processing-options). -The new `addComposerUploadPreProcessor` adds preprocessors for client side -uploads in the form of an Uppy plugin. See https://uppy.io/docs/writing-plugins/ -for the Uppy documentation, but other examples of preprocessors in core can be found -in the UppyMediaOptimization and UppyChecksum classes. This has been done because -of the overarching move towards Uppy in the Discourse codebase rather than -jQuery fileupload, which will eventually be removed altogether as a broader effort -to remove jQuery from the codebase. + favour of `addComposerUploadPreProcessor`. The former was used to add preprocessors + for client side uploads via jQuery file uploader (described at + https://github.com/blueimp/jQuery-File-Upload/wiki/Options#file-processing-options). + The new `addComposerUploadPreProcessor` adds preprocessors for client side + uploads in the form of an Uppy plugin. See https://uppy.io/docs/writing-plugins/ + for the Uppy documentation, but other examples of preprocessors in core can be found + in the UppyMediaOptimization and UppyChecksum classes. This has been done because + of the overarching move towards Uppy in the Discourse codebase rather than + jQuery fileupload, which will eventually be removed altogether as a broader effort + to remove jQuery from the codebase. ### Changed + - Changes `addComposerUploadHandler`'s behaviour. Instead of being only usable -for single files at a time, now multiple files are sent to the upload handler -at once. These multiple files are sent based on the groups in which they are -added (e.g. multiple files selected from the system upload dialog, or multiple -files dropped in to the composer). Files will be sent in buckets to the handlers -they match. + for single files at a time, now multiple files are sent to the upload handler + at once. These multiple files are sent based on the groups in which they are + added (e.g. multiple files selected from the system upload dialog, or multiple + files dropped in to the composer). Files will be sent in buckets to the handlers + they match. From 1341baaebaf9932952cf3fd65448564473188525 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Thu, 24 Mar 2022 09:49:56 +1000 Subject: [PATCH 011/195] DEV: Use composerEventPrefix for paste in textarea-text-manipulation (#16262) In the commit d678ba11030e77808d59c0d8d425286cf399447e we added gif parsing support on paste, but we also slightly changed the isComposer check there, along with a change in chat this caused isComposer to be true (which is correct), however the event we fire is composer:insert-text which the chat composer does not pick up. Instead, we should use composerEventPrefix if it is present to fire the insert-text event, and if it is not present (e.g. for some custom composer that someone has implemented) fall back to the default. There is a companion commit for chat to handle this change there. --- .../app/components/composer-editor.js | 3 ++ .../app/mixins/composer-upload-uppy.js | 38 ++++++++++--------- .../app/mixins/textarea-text-manipulation.js | 17 ++++++++- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index eed18fe3b1..7131223209 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -103,7 +103,10 @@ export default Component.extend(ComposerUploadUppy, { editorClass: ".d-editor", fileUploadElementId: "file-uploader", mobileFileUploaderId: "mobile-file-upload", + + // TODO (martin) Remove this once the chat plugin is using the new composerEventPrefix eventPrefix: "composer", + composerEventPrefix: "composer", uploadType: "composer", uppyId: "composer-editor-uppy", composerModel: alias("composer"), diff --git a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js index 0442cf9697..929ab63c14 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -67,9 +67,9 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.editorEl?.removeEventListener("paste", this.pasteEventListener); - this.appEvents.off(`${this.eventPrefix}:add-files`, this._addFiles); + this.appEvents.off(`${this.composerEventPrefix}:add-files`, this._addFiles); this.appEvents.off( - `${this.eventPrefix}:cancel-upload`, + `${this.composerEventPrefix}:cancel-upload`, this._cancelSingleUpload ); @@ -84,7 +84,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { }, _abortAndReset() { - this.appEvents.trigger(`${this.eventPrefix}:uploads-aborted`); + this.appEvents.trigger(`${this.composerEventPrefix}:uploads-aborted`); this._reset(); return false; }, @@ -97,9 +97,9 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.fileInputEl = document.getElementById(this.fileUploadElementId); const isPrivateMessage = this.get("composerModel.privateMessage"); - this.appEvents.on(`${this.eventPrefix}:add-files`, this._addFiles); + this.appEvents.on(`${this.composerEventPrefix}:add-files`, this._addFiles); this.appEvents.on( - `${this.eventPrefix}:cancel-upload`, + `${this.composerEventPrefix}:cancel-upload`, this._cancelSingleUpload ); @@ -136,7 +136,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { }); if (!isUploading) { - this.appEvents.trigger(`${this.eventPrefix}:uploads-aborted`); + this.appEvents.trigger(`${this.composerEventPrefix}:uploads-aborted`); } return isUploading; }, @@ -290,11 +290,11 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { uploadPlaceholder: placeholder, }; this.appEvents.trigger( - `${this.eventPrefix}:insert-text`, + `${this.composerEventPrefix}:insert-text`, placeholder ); this.appEvents.trigger( - `${this.eventPrefix}:upload-started`, + `${this.composerEventPrefix}:upload-started`, file.name ); }); @@ -316,14 +316,14 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { cacheShortUploadUrl(upload.short_url, upload); this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, + `${this.composerEventPrefix}:replace-text`, this.placeholders[file.id].uploadPlaceholder.trim(), markdown ); this._resetUpload(file, { removePlaceholder: false }); this.appEvents.trigger( - `${this.eventPrefix}:upload-success`, + `${this.composerEventPrefix}:upload-success`, file.name, upload ); @@ -334,7 +334,9 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this._uppyInstance.on("complete", () => { run(() => { - this.appEvents.trigger(`${this.eventPrefix}:all-uploads-complete`); + this.appEvents.trigger( + `${this.composerEventPrefix}:all-uploads-complete` + ); this._reset(); }); }); @@ -346,7 +348,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { Object.values(this.placeholders).forEach((data) => { run(() => { this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, + `${this.composerEventPrefix}:replace-text`, data.uploadPlaceholder, "" ); @@ -356,7 +358,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.set("userCancelled", false); this._reset(); - this.appEvents.trigger(`${this.eventPrefix}:uploads-cancelled`); + this.appEvents.trigger(`${this.composerEventPrefix}:uploads-cancelled`); } }); @@ -381,7 +383,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { if (!this.userCancelled) { displayErrorForUpload(response || error, this.siteSettings, file.name); - this.appEvents.trigger(`${this.eventPrefix}:upload-error`, file); + this.appEvents.trigger(`${this.composerEventPrefix}:upload-error`, file); } if (this.inProgressUploads.length === 0) { @@ -434,7 +436,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { )}]()\n`; this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, + `${this.composerEventPrefix}:replace-text`, placeholderData.uploadPlaceholder, placeholderData.processingPlaceholder ); @@ -445,7 +447,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { run(() => { let placeholderData = this.placeholders[file.id]; this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, + `${this.composerEventPrefix}:replace-text`, placeholderData.processingPlaceholder, placeholderData.uploadPlaceholder ); @@ -458,7 +460,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { isCancellable: true, }); this.appEvents.trigger( - `${this.eventPrefix}:uploads-preprocessing-complete` + `${this.composerEventPrefix}:uploads-preprocessing-complete` ); }); } @@ -536,7 +538,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { _resetUpload(file, opts) { if (opts.removePlaceholder) { this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, + `${this.composerEventPrefix}:replace-text`, this.placeholders[file.id].uploadPlaceholder, "" ); diff --git a/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js b/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js index ab6aeab38a..bb5c5dfd2d 100644 --- a/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js +++ b/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js @@ -34,6 +34,13 @@ export function getHead(head, prev) { export default Mixin.create({ init() { this._super(...arguments); + + // fallback in the off chance someone has implemented a custom composer + // which does not define this + if (!this.composerEventPrefix) { + this.composerEventPrefix = "composer"; + } + generateLinkifyFunction(this.markdownOptions || {}).then((linkify) => { // When pasting links, we should use the same rules to match links as we do when creating links for a cooked post. this._cachedLinkify = linkify; @@ -456,7 +463,10 @@ export default Mixin.create({ plainText = plainText.replace(/\r/g, ""); const table = this._extractTable(plainText); if (table) { - this.appEvents.trigger("composer:insert-text", table); + this.appEvents.trigger( + `${this.composerEventPrefix}:insert-text`, + table + ); handled = true; } } @@ -508,7 +518,10 @@ export default Mixin.create({ } if (isComposer) { - this.appEvents.trigger("composer:insert-text", markdown); + this.appEvents.trigger( + `${this.composerEventPrefix}:insert-text`, + markdown + ); handled = true; } } From 817035b557d6c0c464ada47db177236fc0d25b14 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Thu, 24 Mar 2022 14:50:18 +1000 Subject: [PATCH 012/195] DEV: Add useUploadPlaceholders to composer-upload-uppy (#16272) This option is being added because some composer derivatives like the chat composer use ComposerUploadUppy, but do not need the placeholder text for uploads to be inserted/replaced. This way those components can set useUploadPlaceholders to false to avoid it. --- .../app/mixins/composer-upload-uppy.js | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js index 929ab63c14..4023ba0b7c 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -37,6 +37,7 @@ import { run } from "@ember/runloop"; export default Mixin.create(ExtendableUploader, UppyS3Multipart, { uploadRootPath: "/uploads", uploadTargetBound: false, + useUploadPlaceholders: true, @bind _cancelSingleUpload(data) { @@ -289,10 +290,14 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.placeholders[file.id] = { uploadPlaceholder: placeholder, }; - this.appEvents.trigger( - `${this.composerEventPrefix}:insert-text`, - placeholder - ); + + if (this.useUploadPlaceholders) { + this.appEvents.trigger( + `${this.composerEventPrefix}:insert-text`, + placeholder + ); + } + this.appEvents.trigger( `${this.composerEventPrefix}:upload-started`, file.name @@ -315,11 +320,13 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { cacheShortUploadUrl(upload.short_url, upload); - this.appEvents.trigger( - `${this.composerEventPrefix}:replace-text`, - this.placeholders[file.id].uploadPlaceholder.trim(), - markdown - ); + if (this.useUploadPlaceholders) { + this.appEvents.trigger( + `${this.composerEventPrefix}:replace-text`, + this.placeholders[file.id].uploadPlaceholder.trim(), + markdown + ); + } this._resetUpload(file, { removePlaceholder: false }); this.appEvents.trigger( @@ -347,11 +354,13 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { if (this.userCancelled) { Object.values(this.placeholders).forEach((data) => { run(() => { - this.appEvents.trigger( - `${this.composerEventPrefix}:replace-text`, - data.uploadPlaceholder, - "" - ); + if (this.useUploadPlaceholders) { + this.appEvents.trigger( + `${this.composerEventPrefix}:replace-text`, + data.uploadPlaceholder, + "" + ); + } }); }); From 8dd6cb14eec3d3879e18dbf4a2785d09674a3dd1 Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Thu, 24 Mar 2022 08:33:17 +0300 Subject: [PATCH 013/195] FIX: Don't attempt to focus .title in topic-list-item if it doesn't exist (#16274) Follow-up to https://github.com/discourse/discourse/commit/97e7bb1ce483498640f512c1bf9bfe704dc1d402 Themes/plugins may override the default `topic-list-item` and remove the `.main-link` or `.title` elements from the template. We shouldn't attempt to focus them if they don't exist. --- .../javascripts/discourse/app/components/topic-list-item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/app/components/topic-list-item.js b/app/assets/javascripts/discourse/app/components/topic-list-item.js index 806da566fd..4bdbd7de4b 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -285,7 +285,7 @@ export default Component.extend({ this.element.classList.remove("highlighted"); }); if (opts.isLastViewedTopic && this._shouldFocusLastVisited()) { - this._titleElement().focus(); + this._titleElement()?.focus(); } }); }, From 9d5737fd28374cc876c070f6c3a931a8071ec356 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Thu, 24 Mar 2022 15:38:44 +1000 Subject: [PATCH 014/195] SECURITY: Hide private categories in user activity export (#16273) In some of the user's own activity export data, we sometimes showed a secure category's name or exposed the existence of a secure category. --- app/jobs/regular/export_user_archive.rb | 28 +++++++-------- spec/jobs/export_user_archive_spec.rb | 45 ++++++++++++++++++++----- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/app/jobs/regular/export_user_archive.rb b/app/jobs/regular/export_user_archive.rb index ba2442df77..489dac28b2 100644 --- a/app/jobs/regular/export_user_archive.rb +++ b/app/jobs/regular/export_user_archive.rb @@ -261,15 +261,16 @@ module Jobs CategoryUser .where(user_id: @current_user.id) - .select(:category_id, :notification_level, :last_seen_at) + .includes(:category) + .merge(Category.secured(guardian)) .each do |cu| - yield [ - cu.category_id, - piped_category_name(cu.category_id), - NotificationLevels.all[cu.notification_level], - cu.last_seen_at - ] - end + yield [ + cu.category_id, + piped_category_name(cu.category_id, cu.category), + NotificationLevels.all[cu.notification_level], + cu.last_seen_at + ] + end end def flags_export @@ -408,10 +409,9 @@ module Jobs @guardian ||= Guardian.new(@current_user) end - def piped_category_name(category_id) - return "-" unless category_id - category = Category.find_by(id: category_id) - return "#{category_id}" unless category + def piped_category_name(category_id, category) + return "#{category_id}" if category_id && !category + return "-" if !guardian.can_see_category?(category) categories = [category.name] while category.parent_category_id && category = category.parent_category categories << category.name @@ -433,10 +433,10 @@ module Jobs user_archive_array = [] topic_data = user_archive.topic user_archive = user_archive.as_json - topic_data = Topic.with_deleted.find_by(id: user_archive['topic_id']) if topic_data.nil? + topic_data = Topic.with_deleted.includes(:category).find_by(id: user_archive['topic_id']) if topic_data.nil? return user_archive_array if topic_data.nil? - categories = piped_category_name(topic_data.category_id) + categories = piped_category_name(topic_data.category_id, topic_data.category) is_pm = topic_data.archetype == "private_message" ? I18n.t("csv_export.boolean_yes") : I18n.t("csv_export.boolean_no") url = "#{Discourse.base_url}/t/#{topic_data.slug}/#{topic_data.id}/#{user_archive['post_number']}" diff --git a/spec/jobs/export_user_archive_spec.rb b/spec/jobs/export_user_archive_spec.rb index ea10fdf945..b9e8514d73 100644 --- a/spec/jobs/export_user_archive_spec.rb +++ b/spec/jobs/export_user_archive_spec.rb @@ -15,7 +15,7 @@ describe Jobs::ExportUserArchive do let(:component) { raise 'component not set' } fab!(:admin) { Fabricate(:admin) } - fab!(:category) { Fabricate(:category_with_definition) } + fab!(:category) { Fabricate(:category_with_definition, name: "User Archive Category") } fab!(:subcategory) { Fabricate(:category_with_definition, parent_category_id: category.id) } fab!(:topic) { Fabricate(:topic, category: category) } let(:post) { Fabricate(:post, user: user, topic: topic) } @@ -168,6 +168,17 @@ describe Jobs::ExportUserArchive do _, csv_out = make_component_csv expect(csv_out).to match cat2_id.to_s end + + it "can export a post from a secure category, obscuring the category name" do + cat2 = Fabricate(:private_category, group: Fabricate(:group), name: "Secret Cat") + topic2 = Fabricate(:topic, category: cat2, user: user, title: "This is a test secure topic") + _post2 = Fabricate(:post, topic: topic2, user: user) + data, csv_out = make_component_csv + expect(csv_out).not_to match "Secret Cat" + expect(data.length).to eq(1) + expect(data[0][:topic_title]).to eq("This is a test secure topic") + expect(data[0][:categories]).to eq("-") + end end context 'preferences' do @@ -314,9 +325,11 @@ describe Jobs::ExportUserArchive do context 'category_preferences' do let(:component) { 'category_preferences' } - let(:subsubcategory) { Fabricate(:category_with_definition, parent_category_id: subcategory.id) } - let(:announcements) { Fabricate(:category_with_definition) } - let(:deleted_category) { Fabricate(:category) } + let(:subsubcategory) { Fabricate(:category_with_definition, parent_category_id: subcategory.id, name: "User Archive Subcategory") } + let(:announcements) { Fabricate(:category_with_definition, name: "Announcements") } + let(:deleted_category) { Fabricate(:category, name: "Deleted Category") } + let(:secure_category_group) { Fabricate(:group) } + let(:secure_category) { Fabricate(:private_category, group: secure_category_group, name: "Super Secret Category") } let(:reset_at) { DateTime.parse('2017-03-01 12:00') } @@ -341,11 +354,12 @@ describe Jobs::ExportUserArchive do deleted_category.destroy! end - it 'correctly exports the CategoryUser table' do + it 'correctly exports the CategoryUser table, excluding deleted categories' do data, _csv_out = make_component_csv - expect(data.find { |r| r['category_id'] == category.id }).to be_nil - expect(data.length).to eq(4) + expect(data.find { |r| r['category_id'] == category.id.to_s }).to be_nil + expect(data.find { |r| r['category_id'] == deleted_category.id.to_s }).to be_nil + expect(data.length).to eq(3) data.sort! { |a, b| a['category_id'].to_i <=> b['category_id'].to_i } expect(data[0][:category_id]).to eq(subcategory.id.to_s) @@ -361,8 +375,23 @@ describe Jobs::ExportUserArchive do expect(data[2][:category_names]).to eq(announcements.name) expect(data[2][:notification_level]).to eq('watching_first_post') expect(data[2][:dismiss_new_timestamp]).to eq('') + end - expect(data[3][:category_names]).to eq(data[3][:category_id]) + it "does not include any secure categories the user does not have access to, even if the user has a CategoryUser record" do + CategoryUser.set_notification_level_for_category(user, NotificationLevels.all[:muted], secure_category.id) + data, _csv_out = make_component_csv + + expect(data.any? { |r| r['category_id'] == secure_category.id.to_s }).to eq(false) + expect(data.length).to eq(3) + end + + it "does include secure categories that the user has access to" do + CategoryUser.set_notification_level_for_category(user, NotificationLevels.all[:muted], secure_category.id) + GroupUser.create!(user: user, group: secure_category_group) + data, _csv_out = make_component_csv + + expect(data.any? { |r| r['category_id'] == secure_category.id.to_s }).to eq(true) + expect(data.length).to eq(4) end end From 0d6bb64c0fb337dfa31c595d4eaf34b3eda806f0 Mon Sep 17 00:00:00 2001 From: OsamaSayegh Date: Mon, 21 Mar 2022 23:40:32 +0300 Subject: [PATCH 015/195] A11Y: Add aria-label to the Replies cell in topics list When tabbing through a topics list like /latest, /unread, /new etc. the Replies column is announced as ` button` by screen readers and it's not clear that number means the topic has that number of replies. This commit adds an `aria-label` so the Replies column to make it clear what that number means. The current copy of the `aria-label` is "This topic has replies". --- .../discourse/app/templates/list/posts-count-column.hbr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr b/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr index a697ff9e01..0087b01566 100644 --- a/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr +++ b/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr @@ -1,5 +1,5 @@ <{{view.tagName}} class='num posts-map posts {{view.likesHeat}} topic-list-data' title='{{view.title}}'> - From 0d4fad67dbf4c470503c640d24255a5dc7de37fa Mon Sep 17 00:00:00 2001 From: OsamaSayegh Date: Tue, 22 Mar 2022 00:17:44 +0300 Subject: [PATCH 016/195] A11Y: Add `aria-label`s to topics list column headers Topics lists like /latest are ordered by last activity date by default, but the order can be changed (and reversed) to something else such as replies count and views count by clicking on the corresponding column header in the topics list. These column headers are tabbable, but screen readers announce them as, using the replies column as example, `Replies toggle button`. This doesn't communicate very well that this the button changes the order, so this commit adds `aria-label`s to all column headers to make it clear that they change order. The current copy for the `aria-label` is `Sort by replies`. --- .../discourse/app/templates/topic-list-header.hbr | 10 +++++----- config/locales/client.en.yml | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/app/templates/topic-list-header.hbr b/app/assets/javascripts/discourse/app/templates/topic-list-header.hbr index 636225a3b1..cdca90b5c5 100644 --- a/app/assets/javascripts/discourse/app/templates/topic-list-header.hbr +++ b/app/assets/javascripts/discourse/app/templates/topic-list-header.hbr @@ -10,13 +10,13 @@ {{#if showPosters}} {{raw "topic-list-header-column" order='posters' ariaLabel=(i18n "category.sort_options.posters")}} {{/if}} -{{raw "topic-list-header-column" sortable=sortable number='true' order='posts' name='replies'}} +{{raw "topic-list-header-column" sortable=sortable number='true' order='posts' name='replies' ariaLabel=(i18n "sr_replies")}} {{#if showLikes}} - {{raw "topic-list-header-column" sortable=sortable number='true' order='likes' name='likes'}} + {{raw "topic-list-header-column" sortable=sortable number='true' order='likes' name='likes' ariaLabel=(i18n "sr_likes")}} {{/if}} {{#if showOpLikes}} - {{raw "topic-list-header-column" sortable=sortable number='true' order='op_likes' name='likes'}} + {{raw "topic-list-header-column" sortable=sortable number='true' order='op_likes' name='likes' ariaLabel=(i18n "sr_op_likes")}} {{/if}} -{{raw "topic-list-header-column" sortable=sortable number='true' order='views' name='views'}} -{{raw "topic-list-header-column" sortable=sortable number='true' order='activity' name='activity'}} +{{raw "topic-list-header-column" sortable=sortable number='true' order='views' name='views' ariaLabel=(i18n "sr_views")}} +{{raw "topic-list-header-column" sortable=sortable number='true' order='activity' name='activity' ariaLabel=(i18n "sr_activity")}} {{~raw-plugin-outlet name="topic-list-header-after"~}} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 9991936f0b..193086e544 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3542,15 +3542,20 @@ en: other {}} original_post: "Original Post" views: "Views" + sr_views: "Sort by views" views_lowercase: one: "view" other: "views" replies: "Replies" + sr_replies: "Sort by replies" views_long: one: "this topic has been viewed %{count} time" other: "this topic has been viewed %{number} times" activity: "Activity" + sr_activity: "Sort by activity" likes: "Likes" + sr_likes: "Sort by likes" + sr_op_likes: "Sort by original post likes" likes_lowercase: one: "like" other: "likes" From bc54b0055cba2514b2552d11e9181e0f9684b5a9 Mon Sep 17 00:00:00 2001 From: OsamaSayegh Date: Tue, 22 Mar 2022 11:08:31 +0300 Subject: [PATCH 017/195] A11Y: Improve topic entrance modal Clicking the Replies cell of a topic in a topics list shows a little modal with 2 buttons that take you to the first and last posts of the topic. This modal is currently completely inaccessible to keyboard/screen reader users because it can't be reached using the keyboard. This commit improves the modal so that it traps focus when it's shown and makes it possible to close the modal using the esc key. --- .../app/components/topic-entrance.js | 62 ++++++++++++++++++- .../templates/components/topic-entrance.hbs | 4 +- .../tests/acceptance/topic-entrance-test.js | 26 ++++++++ config/locales/client.en.yml | 3 + 4 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/discourse/tests/acceptance/topic-entrance-test.js diff --git a/app/assets/javascripts/discourse/app/components/topic-entrance.js b/app/assets/javascripts/discourse/app/components/topic-entrance.js index 2b73f4de6e..6cfe3d4fb6 100644 --- a/app/assets/javascripts/discourse/app/components/topic-entrance.js +++ b/app/assets/javascripts/discourse/app/components/topic-entrance.js @@ -2,7 +2,7 @@ import CleansUp from "discourse/mixins/cleans-up"; import Component from "@ember/component"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; -import discourseComputed from "discourse-common/utils/decorators"; +import discourseComputed, { bind } from "discourse-common/utils/decorators"; import { scheduleOnce } from "@ember/runloop"; function entranceDate(dt, showTime) { @@ -31,9 +31,11 @@ function entranceDate(dt, showTime) { export default Component.extend(CleansUp, { elementId: "topic-entrance", classNameBindings: ["visible::hidden"], - _position: null, topic: null, visible: null, + _position: null, + _originalActiveElement: null, + _activeButton: null, @discourseComputed("topic.created_at") createdDate: (createdAt) => new Date(createdAt), @@ -74,12 +76,64 @@ export default Component.extend(CleansUp, { $self.css(pos); }, + @bind + _escListener(e) { + if (e.key === "Escape") { + this.cleanUp(); + } else if (e.key === "Tab") { + if (this._activeButton === "top") { + this._jumpBottomButton().focus(); + this._activeButton = "bottom"; + e.preventDefault(); + } else if (this._activeButton === "bottom") { + this._jumpTopButton().focus(); + this._activeButton = "top"; + e.preventDefault(); + } + } + }, + + _jumpTopButton() { + return this.element.querySelector(".jump-top"); + }, + + _jumpBottomButton() { + return this.element.querySelector(".jump-bottom"); + }, + + _setupEscListener() { + document.body.addEventListener("keydown", this._escListener); + }, + + _removeEscListener() { + document.body.removeEventListener("keydown", this._escListener); + }, + + _trapFocus() { + this._originalActiveElement = document.activeElement; + this._jumpTopButton().focus(); + this._activeButton = "top"; + }, + + _releaseFocus() { + if (this._originalActiveElement) { + this._originalActiveElement.focus(); + this._originalActiveElement = null; + } + }, + + _applyDomChanges() { + this._setCSS(); + this._setupEscListener(); + this._trapFocus(); + }, + _show(data) { this._position = data.position; this.setProperties({ topic: data.topic, visible: true }); - scheduleOnce("afterRender", this, this._setCSS); + scheduleOnce("afterRender", this, this._applyDomChanges); $("html") .off("mousedown.topic-entrance") @@ -98,6 +152,8 @@ export default Component.extend(CleansUp, { cleanUp() { this.setProperties({ topic: null, visible: false }); $("html").off("mousedown.topic-entrance"); + this._removeEscListener(); + this._releaseFocus(); }, willDestroyElement() { diff --git a/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs b/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs index c6e9c5cae2..b7947a4f14 100644 --- a/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs @@ -1,7 +1,7 @@ -{{#d-button action=(action "enterTop") class="btn-default full jump-top"}} +{{#d-button action=(action "enterTop") class="btn-default full jump-top" ariaLabel="topic_entrance.sr_jump_top_button"}} {{d-icon "step-backward"}} {{html-safe topDate}} {{/d-button}} -{{#d-button action=(action "enterBottom") class="btn-default full jump-bottom"}} +{{#d-button action=(action "enterBottom") class="btn-default full jump-bottom" ariaLabel="topic_entrance.sr_jump_bottom_button"}} {{html-safe bottomDate}} {{d-icon "step-forward"}} {{/d-button}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-entrance-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-entrance-test.js new file mode 100644 index 0000000000..2839284f4e --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-entrance-test.js @@ -0,0 +1,26 @@ +import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; +import { click, triggerKeyEvent, visit } from "@ember/test-helpers"; +import { test } from "qunit"; + +const ESC_KEYCODE = 27; +acceptance("Topic Entrance Modal", function () { + test("can be closed with the esc key", async function (assert) { + await visit("/"); + await click(".topic-list-item button.posts-map"); + const topicEntrance = query("#topic-entrance"); + assert.ok( + !topicEntrance.classList.contains("hidden"), + "topic entrance modal appears" + ); + assert.equal( + document.activeElement, + topicEntrance.querySelector(".jump-top"), + "the jump top button has focus when the modal is shown" + ); + await triggerKeyEvent(topicEntrance, "keydown", ESC_KEYCODE); + assert.ok( + topicEntrance.classList.contains("hidden"), + "topic entrance modal disappears after pressing esc" + ); + }); +}); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 193086e544..0a85b4f345 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3987,6 +3987,9 @@ en: no_group_messages_title: "No group messages found" + topic_entrance: + sr_jump_top_button: "Jump to the first post" + sr_jump_bottom_button: "Jump to the last post" fullscreen_table: expand_btn: "Expand Table" From 771dddb711b97c10baf9ac5fd23d769f06f38e7e Mon Sep 17 00:00:00 2001 From: OsamaSayegh Date: Wed, 23 Mar 2022 22:18:32 +0300 Subject: [PATCH 018/195] A11Y: Make the views column in topics lists tabbable --- .../discourse/app/templates/list/topic-list-item.hbr | 2 +- config/locales/client.en.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/app/templates/list/topic-list-item.hbr b/app/assets/javascripts/discourse/app/templates/list/topic-list-item.hbr index da667759fa..5c52674fc5 100644 --- a/app/assets/javascripts/discourse/app/templates/list/topic-list-item.hbr +++ b/app/assets/javascripts/discourse/app/templates/list/topic-list-item.hbr @@ -73,7 +73,7 @@ {{/if}} - + {{raw-plugin-outlet name="topic-list-before-view-count"}} {{number topic.views numberKey="views_long"}} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0a85b4f345..e42e076c01 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3550,7 +3550,7 @@ en: sr_replies: "Sort by replies" views_long: one: "this topic has been viewed %{count} time" - other: "this topic has been viewed %{number} times" + other: "this topic has been viewed %{count} times" activity: "Activity" sr_activity: "Sort by activity" likes: "Likes" From 03ad88f2c241062e0bb976b730228f9428edef92 Mon Sep 17 00:00:00 2001 From: Dan Ungureanu Date: Thu, 24 Mar 2022 14:50:44 +0200 Subject: [PATCH 019/195] FIX: Add `errors` field if group update confirmation (#16260) * FIX: Redirect if Discourse-Xhr-Redirect is present `handleRedirect` was passed an wrong argument type (a string) instead of a jqXHR object and missed the fields checked in condition, thus always evaluating to `false`. * FIX: Add `errors` field if group update confirmation An explicit confirmation about the effect of the group update is required if the default notification level changes. Previously, if the confirmation was missing the API endpoint failed silently returning a 200 response code and a `user_count` field. This change ensures that a proper error code is returned (422), a descriptive error message and the additional information in the `user_count` field. This commit also refactors the API endpoint to use the `Discourse-Xhr-Redirect` header to redirect the user if the group is no longer visible. --- .../components/group-manage-save-button.js | 38 ++-- .../javascripts/discourse/app/lib/ajax.js | 13 +- app/controllers/application_controller.rb | 2 +- app/controllers/groups_controller.rb | 179 ++++++++---------- spec/requests/groups_controller_spec.rb | 12 +- 5 files changed, 106 insertions(+), 138 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/group-manage-save-button.js b/app/assets/javascripts/discourse/app/components/group-manage-save-button.js index 3c7e17d634..3999c2b756 100644 --- a/app/assets/javascripts/discourse/app/components/group-manage-save-button.js +++ b/app/assets/javascripts/discourse/app/components/group-manage-save-button.js @@ -1,5 +1,4 @@ import Component from "@ember/component"; -import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; @@ -37,26 +36,7 @@ export default Component.extend({ return group .save(opts) - .then((data) => { - if (data.user_count) { - const controller = showModal("group-default-notifications", { - model: { - count: data.user_count, - }, - }); - - controller.set("onClose", () => { - this.updateExistingUsers = controller.updateExistingUsers; - this.send("save"); - }); - - return; - } - - if (data.route_to) { - DiscourseURL.routeTo(data.route_to); - } - + .then(() => { this.setProperties({ saved: true, updateExistingUsers: null, @@ -66,7 +46,21 @@ export default Component.extend({ this.afterSave(); } }) - .catch(popupAjaxError) + .catch((error) => { + const json = error.jqXHR.responseJSON; + if (error.jqXHR.status === 422 && json.user_count) { + const controller = showModal("group-default-notifications", { + model: { count: json.user_count }, + }); + + controller.set("onClose", () => { + this.updateExistingUsers = controller.updateExistingUsers; + this.send("save"); + }); + } else { + popupAjaxError(error); + } + }) .finally(() => this.set("saving", false)); }, }, diff --git a/app/assets/javascripts/discourse/app/lib/ajax.js b/app/assets/javascripts/discourse/app/lib/ajax.js index d3591a5ef9..4ec61986c9 100644 --- a/app/assets/javascripts/discourse/app/lib/ajax.js +++ b/app/assets/javascripts/discourse/app/lib/ajax.js @@ -29,14 +29,9 @@ export function handleLogoff(xhr) { } } -function handleRedirect(data) { - if ( - data && - data.getResponseHeader && - data.getResponseHeader("Discourse-Xhr-Redirect") - ) { - window.location.replace(data.responseText); - window.location.reload(); +function handleRedirect(xhr) { + if (xhr && xhr.getResponseHeader("Discourse-Xhr-Redirect")) { + window.location = xhr.responseText; } } @@ -99,7 +94,7 @@ export function ajax() { } args.success = (data, textStatus, xhr) => { - handleRedirect(data); + handleRedirect(xhr); handleLogoff(xhr); run(() => { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 97c8f72151..36e4a4394f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -260,7 +260,7 @@ class ApplicationController < ActionController::Base render json: { error: I18n.t(e.error_translation_key) }, status: e.status_code end - def redirect_with_client_support(url, options) + def redirect_with_client_support(url, options = {}) if request.xhr? response.headers['Discourse-Xhr-Redirect'] = 'true' render plain: url diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 153790cfbb..a5ad225ddb 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -150,38 +150,32 @@ class GroupsController < ApplicationController def update group = Group.find(params[:id]) - guardian.ensure_can_edit!(group) unless guardian.can_admin_group?(group) + guardian.ensure_can_edit!(group) if !guardian.can_admin_group?(group) - params_with_permitted = group_params(automatic: group.automatic) - clear_disabled_email_settings(group, params_with_permitted) + group_attributes = group_params(automatic: group.automatic) + reset_group_email_settings_if_disabled!(group, group_attributes) categories, tags = [] if !group.automatic || current_user.admin - notification_level, categories, tags = user_default_notifications(group, params_with_permitted) + notification_level, categories, tags = user_default_notifications(group, group_attributes) if params[:update_existing_users].blank? user_count = count_existing_users(group.group_users, notification_level, categories, tags) - - if user_count > 0 - render json: { user_count: user_count } - return - end + return render status: 422, json: { user_count: user_count, errors: [I18n.t('invalid_params', message: :update_existing_users)] } if user_count > 0 end end - if group.update(params_with_permitted) + if group.update(group_attributes) GroupActionLogger.new(current_user, group).log_change_group_settings group.record_email_setting_changes!(current_user) group.expire_imap_mailbox_cache update_existing_users(group.group_users, notification_level, categories, tags) if params[:update_existing_users] == "true" AdminDashboardData.clear_found_problem("group_#{group.id}_email_credentials") - if guardian.can_see?(group) - render json: success_json - else - # They can no longer see the group after changing permissions - render json: { route_to: '/g' } - end + # Redirect user to groups index page if they can no longer see the group + return redirect_with_client_support groups_path if !guardian.can_see?(group) + + render json: success_json else render_json_error(group) end @@ -658,87 +652,74 @@ class GroupsController < ApplicationController end def group_params(automatic: false) - permitted_params = - if automatic - %i{ - visibility_level - mentionable_level - messageable_level - default_notification_level - bio_raw - flair_icon - flair_upload_id - flair_bg_color - flair_color - } - else - default_params = %i{ - mentionable_level - messageable_level - title - flair_icon - flair_upload_id - flair_bg_color - flair_color - bio_raw - public_admission - public_exit - allow_membership_requests - full_name - default_notification_level - membership_request_template - } + attributes = %i{ + bio_raw + default_notification_level + messageable_level + mentionable_level + flair_bg_color + flair_color + flair_icon + flair_upload_id + } - if current_user.staff? - default_params.push(*[ - :incoming_email, - :smtp_server, - :smtp_port, - :smtp_ssl, - :smtp_enabled, - :smtp_updated_by, - :smtp_updated_at, - :imap_server, - :imap_port, - :imap_ssl, - :imap_mailbox_name, - :imap_enabled, - :imap_updated_by, - :imap_updated_at, - :email_username, - :email_password, - :email_from_alias, - :primary_group, - :visibility_level, - :members_visibility_level, - :name, - :grant_trust_level, - :automatic_membership_email_domains, - :publish_read_state, - :allow_unknown_sender_topic_replies - ]) + if automatic + attributes.push(:visibility_level) + else + attributes.push( + :title, + :allow_membership_requests, + :full_name, + :public_exit, + :public_admission, + :membership_request_template + ) + end - custom_fields = DiscoursePluginRegistry.editable_group_custom_fields - default_params << { custom_fields: custom_fields } unless custom_fields.blank? - end + if !automatic && current_user.staff? + attributes.push( + :incoming_email, + :smtp_server, + :smtp_port, + :smtp_ssl, + :smtp_enabled, + :smtp_updated_by, + :smtp_updated_at, + :imap_server, + :imap_port, + :imap_ssl, + :imap_mailbox_name, + :imap_enabled, + :imap_updated_by, + :imap_updated_at, + :email_username, + :email_password, + :email_from_alias, + :primary_group, + :visibility_level, + :members_visibility_level, + :name, + :grant_trust_level, + :automatic_membership_email_domains, + :publish_read_state, + :allow_unknown_sender_topic_replies + ) - default_params - end + custom_fields = DiscoursePluginRegistry.editable_group_custom_fields + attributes << { custom_fields: custom_fields } if custom_fields.present? + end if !automatic || current_user.admin [:muted, :regular, :tracking, :watching, :watching_first_post].each do |level| - permitted_params << { "#{level}_category_ids" => [] } - permitted_params << { "#{level}_tags" => [] } + attributes << { "#{level}_category_ids" => [] } + attributes << { "#{level}_tags" => [] } end end - if guardian.can_associate_groups? - permitted_params << { associated_group_ids: [] } - end + attributes << { associated_group_ids: [] } if guardian.can_associate_groups? + attributes.concat(DiscoursePluginRegistry.group_params) - permitted_params = permitted_params | DiscoursePluginRegistry.group_params - - params.require(:group).permit(*permitted_params) + params.require(:group).permit(*attributes) end def find_group(param_name, ensure_can_see: true) @@ -767,23 +748,23 @@ class GroupsController < ApplicationController users end - def clear_disabled_email_settings(group, params_with_permitted) - should_clear_imap = group.imap_enabled && params_with_permitted.key?(:imap_enabled) && params_with_permitted[:imap_enabled] == "false" - should_clear_smtp = group.smtp_enabled && params_with_permitted.key?(:smtp_enabled) && params_with_permitted[:smtp_enabled] == "false" + def reset_group_email_settings_if_disabled!(group, attributes) + should_clear_imap = group.imap_enabled && attributes[:imap_enabled] == "false" + should_clear_smtp = group.smtp_enabled && attributes[:smtp_enabled] == "false" if should_clear_imap || should_clear_smtp - params_with_permitted[:imap_server] = nil - params_with_permitted[:imap_ssl] = false - params_with_permitted[:imap_port] = nil - params_with_permitted[:imap_mailbox_name] = "" + attributes[:imap_server] = nil + attributes[:imap_ssl] = false + attributes[:imap_port] = nil + attributes[:imap_mailbox_name] = "" end if should_clear_smtp - params_with_permitted[:smtp_server] = nil - params_with_permitted[:smtp_ssl] = false - params_with_permitted[:smtp_port] = nil - params_with_permitted[:email_username] = nil - params_with_permitted[:email_password] = nil + attributes[:smtp_server] = nil + attributes[:smtp_ssl] = false + attributes[:smtp_port] = nil + attributes[:email_username] = nil + attributes[:email_password] = nil end end diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index fd29c6741f..de23c58a79 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -904,14 +904,12 @@ describe GroupsController do } } - expect(response.status).to eq(200) - + expect(response.status).to eq(422) + expect(response.parsed_body["user_count"]).to eq(group.group_users.count) + expect(response.parsed_body["errors"].first).to include("update_existing_users") expect(group_user1.reload.notification_level).to eq(NotificationLevels.all[:watching]) expect(group_user2.reload.notification_level).to eq(NotificationLevels.all[:watching]) - group_users = group.group_users - expect(response.parsed_body["user_count"]).to eq(group_users.count) - group_user1.update!(notification_level: NotificationLevels.all[:regular]) put "/groups/#{group.id}.json", params: { @@ -920,7 +918,7 @@ describe GroupsController do } } - expect(response.status).to eq(200) + expect(response.status).to eq(422) expect(response.parsed_body["user_count"]).to eq(group.group_users.count - 1) expect(group_user1.reload.notification_level).to eq(NotificationLevels.all[:regular]) expect(group_user2.reload.notification_level).to eq(NotificationLevels.all[:watching]) @@ -973,7 +971,7 @@ describe GroupsController do } } - expect(response.status).to eq(200) + expect(response.status).to eq(422) expect(response.parsed_body["user_count"]).to eq(group.group_users.count - 1) put "/groups/#{group.id}.json", params: { From 5423d4644201c94129d3590375bb5aed44681141 Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Thu, 24 Mar 2022 21:20:55 +0100 Subject: [PATCH 020/195] UX: cleaner messages for empty state on the user activity topics page (#16267) --- .../discourse/app/routes/discourse.js | 9 +++++ .../app/routes/user-activity-drafts.js | 2 +- .../app/routes/user-activity-stream.js | 2 +- .../app/routes/user-activity-topics.js | 9 ++++- .../acceptance/user-activity-topic-test.js | 37 +++++++++++++++---- config/locales/client.en.yml | 1 + 6 files changed, 49 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/discourse/app/routes/discourse.js b/app/assets/javascripts/discourse/app/routes/discourse.js index eb2b4f3c43..9890e5c2ca 100644 --- a/app/assets/javascripts/discourse/app/routes/discourse.js +++ b/app/assets/javascripts/discourse/app/routes/discourse.js @@ -74,6 +74,7 @@ const DiscourseRoute = Route.extend({ } }, + // deprecated, use isCurrentUser() instead isAnotherUsersPage(user) { if (!this.currentUser) { return true; @@ -82,6 +83,14 @@ const DiscourseRoute = Route.extend({ return user.username !== this.currentUser.username; }, + isCurrentUser(user) { + if (!this.currentUser) { + return false; // the current user is anonymous + } + + return user.id === this.currentUser.id; + }, + isPoppedState(transition) { return !transition._discourse_intercepted && !!transition.intent.url; }, diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js b/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js index 0eaf79c8b0..67b73d3634 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js @@ -11,7 +11,7 @@ export default DiscourseRoute.extend({ return draftsStream.findItems(this.site).then(() => { return { stream: draftsStream, - isAnotherUsersPage: this.isAnotherUsersPage(user), + isAnotherUsersPage: !this.isCurrentUser(user), emptyState: this.emptyState(), }; }); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-stream.js b/app/assets/javascripts/discourse/app/routes/user-activity-stream.js index 69fabb3173..f0e1287d37 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-stream.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-stream.js @@ -16,7 +16,7 @@ export default DiscourseRoute.extend(ViewingActionType, { return { stream, - isAnotherUsersPage: this.isAnotherUsersPage(user), + isAnotherUsersPage: !this.isCurrentUser(user), emptyState: this.emptyState(), emptyStateOthers: this.emptyStateOthers, }; diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-topics.js b/app/assets/javascripts/discourse/app/routes/user-activity-topics.js index 9e48ca30d5..bb8ee51b78 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-topics.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-topics.js @@ -23,8 +23,15 @@ export default UserTopicListRoute.extend({ }, emptyState() { + const user = this.modelFor("user"); + const title = this.isCurrentUser(user) + ? I18n.t("user_activity.no_topics_title") + : I18n.t("user_activity.no_topics_title_others", { + username: user.username, + }); + return { - title: I18n.t("user_activity.no_topics_title"), + title, body: "", }; }, diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-activity-topic-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-activity-topic-test.js index e669d1e20b..673db76cbb 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-activity-topic-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-activity-topic-test.js @@ -1,14 +1,18 @@ -import { acceptance, exists, queryAll } from "../helpers/qunit-helpers"; +import { acceptance, exists, query, queryAll } from "../helpers/qunit-helpers"; import { test } from "qunit"; import { click, visit } from "@ember/test-helpers"; import userFixtures from "../fixtures/user-fixtures"; +import I18n from "I18n"; acceptance("User Activity / Topics - bulk actions", function (needs) { + const currentUser = "eviltrout"; needs.user(); needs.pretender((server, helper) => { - server.get("/topics/created-by/:username.json", () => { - return helper.response(userFixtures["/topics/created-by/eviltrout.json"]); + server.get(`/topics/created-by/${currentUser}.json`, () => { + return helper.response( + userFixtures[`/topics/created-by/${currentUser}.json`] + ); }); server.put("/topics/bulk", () => { @@ -17,7 +21,7 @@ acceptance("User Activity / Topics - bulk actions", function (needs) { }); test("bulk topic closing works", async function (assert) { - await visit("/u/charlie/activity/topics"); + await visit(`/u/${currentUser}/activity/topics`); await click("button.bulk-select"); await click(queryAll("input.bulk-select")[0]); @@ -34,6 +38,8 @@ acceptance("User Activity / Topics - bulk actions", function (needs) { }); acceptance("User Activity / Topics - empty state", function (needs) { + const currentUser = "eviltrout"; + const anotherUser = "charlie"; needs.user(); needs.pretender((server, helper) => { @@ -43,13 +49,28 @@ acceptance("User Activity / Topics - empty state", function (needs) { }, }; - server.get("/topics/created-by/:username.json", () => { + server.get(`/topics/created-by/${currentUser}.json`, () => { + return helper.response(emptyResponse); + }); + + server.get(`/topics/created-by/${anotherUser}.json`, () => { return helper.response(emptyResponse); }); }); - test("It renders the empty state panel", async function (assert) { - await visit("/u/charlie/activity/topics"); - assert.ok(exists("div.empty-state")); + test("When looking at the own activity page", async function (assert) { + await visit(`/u/${currentUser}/activity/topics`); + assert.equal( + query("div.empty-state span.empty-state-title").innerText, + I18n.t("user_activity.no_topics_title") + ); + }); + + test("When looking at another user's activity page", async function (assert) { + await visit(`/u/${anotherUser}/activity/topics`); + assert.equal( + query("div.empty-state span.empty-state-title").innerText, + I18n.t("user_activity.no_topics_title_others", { username: anotherUser }) + ); }); }); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e42e076c01..a18aad5903 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3982,6 +3982,7 @@ en: no_likes_body: "A great way to jump in and start contributing is to start reading conversations that have already taken place, and select the %{heartIcon} on posts that you like!" no_likes_others: "No liked posts." no_topics_title: "You have not started any topics yet" + no_topics_title_others: "%{username} has not started any topics yet" no_read_topics_title: "You haven’t read any topics yet" no_read_topics_body: "Once you start reading discussions, you’ll see a list here. To start reading, look for topics that interest you in Top or Categories or search by keyword %{searchIcon}" From c5508a579099d6644c2a445ba405098dab209dbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Mar 2022 23:26:49 +0100 Subject: [PATCH 021/195] Build(deps): Bump uniform_notifier from 1.15.0 to 1.16.0 (#16281) Bumps [uniform_notifier](https://github.com/flyerhzm/uniform_notifier) from 1.15.0 to 1.16.0. - [Release notes](https://github.com/flyerhzm/uniform_notifier/releases) - [Changelog](https://github.com/flyerhzm/uniform_notifier/blob/master/CHANGELOG.md) - [Commits](https://github.com/flyerhzm/uniform_notifier/compare/v1.15.0...v1.16.0) --- updated-dependencies: - dependency-name: uniform_notifier dependency-type: indirect update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8eb6a18971..e38ce3b71c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -466,7 +466,7 @@ GEM unicorn (6.1.0) kgio (~> 2.6) raindrops (~> 0.7) - uniform_notifier (1.15.0) + uniform_notifier (1.16.0) uri_template (0.7.0) webmock (3.14.0) addressable (>= 2.8.0) From 76ece494f93332d991d72e18dc6f2de88f92ce31 Mon Sep 17 00:00:00 2001 From: Dan Ungureanu Date: Fri, 25 Mar 2022 03:07:21 +0200 Subject: [PATCH 022/195] DEV: Fix "serialize to JSON safely" deprecation (#16280) Job arguments must match after a serialize-deserialize cycle and symbols were converted to strings during this process. --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 0ab48be074..5d69ccede9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -428,7 +428,7 @@ class User < ActiveRecord::Base user_id: id, message_type: 'welcome_staff', message_options: { - role: role + role: role.to_s } ) end From 136f7dbf78c19bd6af0fbd049eb233028312335b Mon Sep 17 00:00:00 2001 From: Roman Rizzi Date: Fri, 25 Mar 2022 09:36:39 -0300 Subject: [PATCH 023/195] DEV: Remove old link building code. (#16121) We have a new API introduced [here](https://github.com/discourse/discourse/pull/14553). --- app/serializers/reviewable_score_serializer.rb | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/app/serializers/reviewable_score_serializer.rb b/app/serializers/reviewable_score_serializer.rb index a16bd752c8..549bd4536a 100644 --- a/app/serializers/reviewable_score_serializer.rb +++ b/app/serializers/reviewable_score_serializer.rb @@ -40,12 +40,6 @@ class ReviewableScoreSerializer < ApplicationSerializer text = I18n.t("reviewables.reasons.#{object.reason}", link: link, default: nil) else text = I18n.t("reviewables.reasons.#{object.reason}", default: nil) - - # TODO(roman): Remove after the 2.8 release. - # The discourse-antivirus and akismet plugins still use the backtick format for settings. - # It'll be hard to migrate them to the new format without breaking backwards compatibility, so I'm keeping the old behavior for now. - # Will remove after the 2.8 release. - linkify_backticks(object.reason, text) if text end text @@ -86,15 +80,4 @@ class ReviewableScoreSerializer < ApplicationSerializer "#{text.gsub('_', ' ')}" end - - def linkify_backticks(reason, text) - text.gsub!(/`[a-z_]+`/) do |m| - if scope.is_staff? - setting = m[1..-2] - "#{setting.gsub('_', ' ')}" - else - m.gsub('_', ' ') - end - end - end end From 9ce6280f516a629fb7ee8fd5d0643fac26097080 Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Fri, 25 Mar 2022 10:44:12 -0500 Subject: [PATCH 024/195] DEV: Make tests more resilient (#16279) Since we give a 200 response for login errors, we should be checking whether the error key exists in each case or not. Some tests were broken, because they weren't checking. --- spec/requests/session_controller_spec.rb | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/spec/requests/session_controller_spec.rb b/spec/requests/session_controller_spec.rb index 842cacc454..c1629a3a02 100644 --- a/spec/requests/session_controller_spec.rb +++ b/spec/requests/session_controller_spec.rb @@ -30,6 +30,7 @@ describe SessionController do user.update(admin: true) get "/session/email-login/#{email_token.token}.json" expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present end end @@ -46,6 +47,7 @@ describe SessionController do user.update(admin: true) get "/session/email-login/#{email_token.token}.json" expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present end end @@ -134,6 +136,7 @@ describe SessionController do user.update(admin: true) post "/session/email-login/#{email_token.token}.json" expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(session[:current_user_id]).to eq(user.id) end end @@ -257,6 +260,7 @@ describe SessionController do it "sets the user_option timezone for the user" do post "/session/email-login/#{email_token.token}.json", params: { timezone: "Australia/Melbourne" } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(user.reload.user_option.timezone).to eq("Australia/Melbourne") end end @@ -421,6 +425,7 @@ describe SessionController do } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present user.reload expect(session[:current_user_id]).to eq(user.id) @@ -1344,12 +1349,14 @@ describe SessionController do context 'local login via email is disabled' do before do SiteSetting.enable_local_logins_via_email = false + EmailToken.confirm(email_token.token) end it 'doesnt matter, logs in correctly' do post "/session.json", params: { login: user.username, password: 'myawesomepassword' } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present end end @@ -1446,6 +1453,7 @@ describe SessionController do end expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(events.map { |event| event[:event_name] }).to contain_exactly( :user_logged_in, :user_first_logged_in ) @@ -1464,6 +1472,7 @@ describe SessionController do login: user.username, password: 'myawesomepassword', timezone: "Australia/Melbourne" } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(user.reload.user_option.timezone).to eq("Australia/Melbourne") end end @@ -1544,6 +1553,7 @@ describe SessionController do } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present user.reload expect(session[:current_user_id]).to eq(user.id) @@ -1638,6 +1648,7 @@ describe SessionController do second_factor_method: UserSecondFactor.methods[:totp] } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present user.reload expect(session[:current_user_id]).to eq(user.id) @@ -1658,6 +1669,7 @@ describe SessionController do second_factor_method: UserSecondFactor.methods[:backup_codes] } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present user.reload expect(session[:current_user_id]).to eq(user.id) @@ -1680,6 +1692,7 @@ describe SessionController do login: "@" + user.username, password: 'myawesomepassword' } expect(response.status).to eq(200) + expect(response.parsed_body['error']).to be_present user.reload expect(session[:current_user_id]).to be_nil @@ -1692,6 +1705,7 @@ describe SessionController do login: "@" + user.username, password: 'myawesomepassword' } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present user.reload expect(session[:current_user_id]).to eq(user.id) @@ -1704,6 +1718,7 @@ describe SessionController do login: user.email, password: 'myawesomepassword' } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(session[:current_user_id]).to eq(user.id) end end @@ -1744,6 +1759,7 @@ describe SessionController do it "doesn't log in the user" do expect(response.status).to eq(200) + expect(response.parsed_body['error']).to be_present expect(session[:current_user_id]).to be_blank end @@ -1764,6 +1780,7 @@ describe SessionController do login: user.email, password: 'myawesomepassword' } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(session[:current_user_id]).to eq(user.id) end end @@ -1786,6 +1803,7 @@ describe SessionController do login: user.username, password: 'myawesomepassword' } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(session[:current_user_id]).to eq(user.id) end @@ -1813,6 +1831,7 @@ describe SessionController do } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(session[:current_user_id]).to eq(user.id) end end @@ -1828,6 +1847,7 @@ describe SessionController do it "doesn't log in the user" do post_login expect(response.status).to eq(200) + expect(response.parsed_body['error']).to be_present expect(session[:current_user_id]).to be_blank end @@ -1857,6 +1877,7 @@ describe SessionController do SiteSetting.max_logins_per_ip_per_hour = 2 RateLimiter.enable RateLimiter.clear_all! + EmailToken.confirm(email_token.token) 2.times do post "/session.json", params: { @@ -1864,6 +1885,7 @@ describe SessionController do } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present end post "/session.json", params: { @@ -1887,6 +1909,7 @@ describe SessionController do second_factor_method: UserSecondFactor.methods[:totp] } expect(response.status).to eq(200) + expect(response.parsed_body['error']).to be_present end post "/session.json", params: { @@ -1904,6 +1927,7 @@ describe SessionController do it 'rate limits second factor attempts by login' do RateLimiter.enable RateLimiter.clear_all! + EmailToken.confirm(email_token.token) 6.times do |x| post "/session.json", params: { @@ -1914,6 +1938,7 @@ describe SessionController do }, env: { "REMOTE_ADDR": "1.2.3.#{x}" } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present end [user.username + " ", user.username.capitalize, user.username].each_with_index do |username , x| @@ -1947,6 +1972,7 @@ describe SessionController do delete "/session/#{user.username}.json", xhr: true expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(session[:current_user_id]).to be_blank expect(response.cookies["_t"]).to be_blank @@ -1960,12 +1986,14 @@ describe SessionController do user = sign_in(Fabricate(:user)) delete "/session/#{user.username}.json", xhr: true expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(response.parsed_body["redirect_url"]).to eq("/") SiteSetting.login_required = true user = sign_in(Fabricate(:user)) delete "/session/#{user.username}.json", xhr: true expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(response.parsed_body["redirect_url"]).to eq("/login") end @@ -1980,6 +2008,7 @@ describe SessionController do delete "/session/#{user.username}.json", xhr: true expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(response.parsed_body["redirect_url"]).to eq("/myredirect/#{user.username}") ensure DiscourseEvent.off(:before_session_destroy, &callback) @@ -2013,6 +2042,7 @@ describe SessionController do get "/session/otp/#{token}" expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(response.body).to include( I18n.t("user_api_key.otp_confirmation.logging_in_as", username: user.username) ) @@ -2050,6 +2080,7 @@ describe SessionController do get "/session/current.json" expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present end end end @@ -2078,6 +2109,7 @@ describe SessionController do params: { login: user.username } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(Jobs::CriticalUserEmail.jobs.size).to eq(1) end @@ -2086,6 +2118,7 @@ describe SessionController do params: { login: user.email } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(Jobs::CriticalUserEmail.jobs.size).to eq(1) end end @@ -2120,6 +2153,7 @@ describe SessionController do 3.times do post "/session/forgot_password.json", params: { login: user.username } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present end post "/session/forgot_password.json", params: { login: user.username } @@ -2131,6 +2165,7 @@ describe SessionController do headers: { 'REMOTE_ADDR' => '10.1.1.1' } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present end post "/session/forgot_password.json", @@ -2253,6 +2288,7 @@ describe SessionController do it "returns the JSON for the user" do get "/session/current.json" expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present json = response.parsed_body expect(json['current_user']).to be_present expect(json['current_user']['id']).to eq(user.id) @@ -2285,6 +2321,7 @@ describe SessionController do nonce = response.parsed_body["second_factor_challenge_nonce"] get "/session/2fa.json", params: { nonce: nonce } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present freeze_time (SecondFactor::AuthManager::MAX_CHALLENGE_AGE + 1.minute).from_now get "/session/2fa.json", params: { nonce: nonce } @@ -2297,6 +2334,7 @@ describe SessionController do nonce = response.parsed_body["second_factor_challenge_nonce"] get "/session/2fa.json", params: { nonce: nonce } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present challenge_data = response.parsed_body expect(challenge_data["totp_enabled"]).to eq(true) expect(challenge_data["backup_enabled"]).to eq(false) @@ -2318,6 +2356,7 @@ describe SessionController do nonce = response.parsed_body["second_factor_challenge_nonce"] get "/session/2fa.json", params: { nonce: nonce } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present challenge_data = response.parsed_body expect(challenge_data["totp_enabled"]).to eq(true) expect(challenge_data["backup_enabled"]).to eq(true) @@ -2390,6 +2429,7 @@ describe SessionController do second_factor_token: token } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(response.parsed_body["ok"]).to eq(true) expect(response.parsed_body["callback_method"]).to eq("POST") expect(response.parsed_body["callback_path"]).to eq("/session/2fa/test-action") @@ -2397,6 +2437,7 @@ describe SessionController do post "/session/2fa/test-action", params: { second_factor_nonce: nonce } expect(response.status).to eq(200) + expect(response.parsed_body['error']).not_to be_present expect(response.parsed_body["result"]).to eq("second_factor_auth_completed") end From f3aab198290ff1bc1a44c39bba7e89d8490c75bb Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 25 Mar 2022 15:48:20 +0000 Subject: [PATCH 025/195] DEV: Promote historic post_deploy migrations (#16288) This commit promotes all post_deploy migrations which existed in Discourse v2.7.13 (timestamp <= 20210328233843) This reduces the likelihood of issues relating to migration run order Also fixes a couple of typos in `script/promote_migrations` --- ...25100452_migrate_search_data_after_default_locale_rename.rb | 0 .../20210127140730_undo_add_processed_to_notifications.rb | 0 .../20210207232853_fix_topic_timer_duration_minutes.rb | 0 ...15231312_fix_group_flair_avatar_upload_security_and_acls.rb | 0 .../20210218022739_move_new_since_to_new_table_again.rb | 0 .../20210219171329_drop_old_sso_site_settings.rb | 0 .../20210302164429_drop_flash_onebox_site_setting.rb | 0 .../20210324043327_delete_orphan_post_revisions.rb | 0 .../20210328233843_fix_bookmarks_with_incorrect_topic_id.rb | 0 script/promote_migrations | 3 ++- 10 files changed, 2 insertions(+), 1 deletion(-) rename db/{post_migrate => migrate}/20210125100452_migrate_search_data_after_default_locale_rename.rb (100%) rename db/{post_migrate => migrate}/20210127140730_undo_add_processed_to_notifications.rb (100%) rename db/{post_migrate => migrate}/20210207232853_fix_topic_timer_duration_minutes.rb (100%) rename db/{post_migrate => migrate}/20210215231312_fix_group_flair_avatar_upload_security_and_acls.rb (100%) rename db/{post_migrate => migrate}/20210218022739_move_new_since_to_new_table_again.rb (100%) rename db/{post_migrate => migrate}/20210219171329_drop_old_sso_site_settings.rb (100%) rename db/{post_migrate => migrate}/20210302164429_drop_flash_onebox_site_setting.rb (100%) rename db/{post_migrate => migrate}/20210324043327_delete_orphan_post_revisions.rb (100%) rename db/{post_migrate => migrate}/20210328233843_fix_bookmarks_with_incorrect_topic_id.rb (100%) diff --git a/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb b/db/migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb similarity index 100% rename from db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb rename to db/migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb diff --git a/db/post_migrate/20210127140730_undo_add_processed_to_notifications.rb b/db/migrate/20210127140730_undo_add_processed_to_notifications.rb similarity index 100% rename from db/post_migrate/20210127140730_undo_add_processed_to_notifications.rb rename to db/migrate/20210127140730_undo_add_processed_to_notifications.rb diff --git a/db/post_migrate/20210207232853_fix_topic_timer_duration_minutes.rb b/db/migrate/20210207232853_fix_topic_timer_duration_minutes.rb similarity index 100% rename from db/post_migrate/20210207232853_fix_topic_timer_duration_minutes.rb rename to db/migrate/20210207232853_fix_topic_timer_duration_minutes.rb diff --git a/db/post_migrate/20210215231312_fix_group_flair_avatar_upload_security_and_acls.rb b/db/migrate/20210215231312_fix_group_flair_avatar_upload_security_and_acls.rb similarity index 100% rename from db/post_migrate/20210215231312_fix_group_flair_avatar_upload_security_and_acls.rb rename to db/migrate/20210215231312_fix_group_flair_avatar_upload_security_and_acls.rb diff --git a/db/post_migrate/20210218022739_move_new_since_to_new_table_again.rb b/db/migrate/20210218022739_move_new_since_to_new_table_again.rb similarity index 100% rename from db/post_migrate/20210218022739_move_new_since_to_new_table_again.rb rename to db/migrate/20210218022739_move_new_since_to_new_table_again.rb diff --git a/db/post_migrate/20210219171329_drop_old_sso_site_settings.rb b/db/migrate/20210219171329_drop_old_sso_site_settings.rb similarity index 100% rename from db/post_migrate/20210219171329_drop_old_sso_site_settings.rb rename to db/migrate/20210219171329_drop_old_sso_site_settings.rb diff --git a/db/post_migrate/20210302164429_drop_flash_onebox_site_setting.rb b/db/migrate/20210302164429_drop_flash_onebox_site_setting.rb similarity index 100% rename from db/post_migrate/20210302164429_drop_flash_onebox_site_setting.rb rename to db/migrate/20210302164429_drop_flash_onebox_site_setting.rb diff --git a/db/post_migrate/20210324043327_delete_orphan_post_revisions.rb b/db/migrate/20210324043327_delete_orphan_post_revisions.rb similarity index 100% rename from db/post_migrate/20210324043327_delete_orphan_post_revisions.rb rename to db/migrate/20210324043327_delete_orphan_post_revisions.rb diff --git a/db/post_migrate/20210328233843_fix_bookmarks_with_incorrect_topic_id.rb b/db/migrate/20210328233843_fix_bookmarks_with_incorrect_topic_id.rb similarity index 100% rename from db/post_migrate/20210328233843_fix_bookmarks_with_incorrect_topic_id.rb rename to db/migrate/20210328233843_fix_bookmarks_with_incorrect_topic_id.rb diff --git a/script/promote_migrations b/script/promote_migrations index 5dea7f0090..be247a9083 100755 --- a/script/promote_migrations +++ b/script/promote_migrations @@ -16,12 +16,13 @@ require 'fileutils' VERSION_REGEX = %r{\/(\d+)_} DRY_RUN = !!ARGV.delete('--dry-run') +PLUGINS = false if i = ARGV.find_index('--plugins-base') ARGV.delete_at(i) PLUGINS = true PLUGINS_BASE = ARGV.delete_at(i) -elsif ARV.delete('--plugins') +elsif ARGV.delete('--plugins') PLUGINS = true PLUGINS_BASE = 'plugins' end From a3563336dbb419a0fc9f6fe9def09c396b4c1c16 Mon Sep 17 00:00:00 2001 From: Mark VanLandingham Date: Fri, 25 Mar 2022 10:51:45 -0500 Subject: [PATCH 026/195] FIX: Bug setting notification level to muted/ignored on user page (#16268) --- .../app/controllers/ignore-duration.js | 2 +- .../discourse/app/controllers/user.js | 23 ++-- .../app/templates/modal/ignore-duration.hbs | 2 +- .../discourse/tests/acceptance/user-test.js | 117 ++++++++++++++++++ app/controllers/users_controller.rb | 2 + config/locales/server.en.yml | 1 + spec/requests/users_controller_spec.rb | 10 ++ 7 files changed, 146 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/discourse/app/controllers/ignore-duration.js b/app/assets/javascripts/discourse/app/controllers/ignore-duration.js index 929581be2e..77e77f3e95 100644 --- a/app/assets/javascripts/discourse/app/controllers/ignore-duration.js +++ b/app/assets/javascripts/discourse/app/controllers/ignore-duration.js @@ -18,7 +18,7 @@ export default Controller.extend(ModalFunctionality, { this.set("loading", true); this.model .updateNotificationLevel({ - level: "ignored", + level: "ignore", expiringAt: this.ignoredUntil, }) .then(() => { diff --git a/app/assets/javascripts/discourse/app/controllers/user.js b/app/assets/javascripts/discourse/app/controllers/user.js index 49199aafd7..9567758f05 100644 --- a/app/assets/javascripts/discourse/app/controllers/user.js +++ b/app/assets/javascripts/discourse/app/controllers/user.js @@ -168,14 +168,19 @@ export default Controller.extend(CanCheckEmails, { "currentUser.ignored_ids", "model.ignored", "model.muted", - function () { - if (this.get("model.ignored")) { - return "changeToIgnored"; - } else if (this.get("model.muted")) { - return "changeToMuted"; - } else { - return "changeToNormal"; - } + { + get() { + if (this.get("model.ignored")) { + return "changeToIgnored"; + } else if (this.get("model.muted")) { + return "changeToMuted"; + } else { + return "changeToNormal"; + } + }, + set(key, value) { + return value; + }, } ), @@ -250,7 +255,7 @@ export default Controller.extend(CanCheckEmails, { }, updateNotificationLevel(level) { - return this.model.updateNotificationLevel({ level }); + return this.model.updateNotificationLevel(level); }, }, }); diff --git a/app/assets/javascripts/discourse/app/templates/modal/ignore-duration.hbs b/app/assets/javascripts/discourse/app/templates/modal/ignore-duration.hbs index cabbb66960..566b60f21a 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/ignore-duration.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/ignore-duration.hbs @@ -11,7 +11,7 @@ {{/d-modal-body}}