From cb922ca8c8498faa5fb2ba862fe49004dd57ef39 Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Thu, 29 Sep 2022 13:38:44 -0500 Subject: [PATCH 001/332] DEV: update .ruby-version.sample (#18426) --- .ruby-version.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version.sample b/.ruby-version.sample index 37c2961c24..49cdd668e1 100644 --- a/.ruby-version.sample +++ b/.ruby-version.sample @@ -1 +1 @@ -2.7.2 +2.7.6 From f60e6837c676af8d9b270cf8a3a308f4a22a2a94 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Fri, 30 Sep 2022 00:49:17 +0300 Subject: [PATCH 002/332] FEATURE: Add setting to always confirm old email (#18417) By default, only staff members have to confirm their old email when changing it. This commit adds a site setting that when enabled will always ask the user to confirm old email. --- config/locales/server.en.yml | 1 + config/site_settings.yml | 1 + lib/email_updater.rb | 3 +-- spec/lib/email_updater_spec.rb | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index fdfa668e77..251474c7f5 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2044,6 +2044,7 @@ en: raw_email_max_length: "How many characters should be stored for incoming email." raw_rejected_email_max_length: "How many characters should be stored for rejected incoming email." delete_rejected_email_after_days: "Delete rejected emails older than (n) days." + require_change_email_confirmation: "Require non-staff users to confirm their old email address before changing it. Does not apply to staff users, they always need to confirm their old email address." manual_polling_enabled: "Push emails using the API for email replies." pop3_polling_enabled: "Poll via POP3 for email replies." diff --git a/config/site_settings.yml b/config/site_settings.yml index 7dc15b9746..ebf5c76a6b 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1291,6 +1291,7 @@ email: max_participant_names: default: 10 hidden: true + require_change_email_confirmation: false files: max_image_size_kb: diff --git a/lib/email_updater.rb b/lib/email_updater.rb index c3e3ea8abf..8f5fb292e2 100644 --- a/lib/email_updater.rb +++ b/lib/email_updater.rb @@ -58,8 +58,7 @@ class EmailUpdater end if @change_req.change_state.blank? || @change_req.change_state == EmailChangeRequest.states[:complete] - @change_req.change_state = if @user.staff? - # Staff users must confirm their old email address first. + @change_req.change_state = if SiteSetting.require_change_email_confirmation || @user.staff? EmailChangeRequest.states[:authorizing_old] else EmailChangeRequest.states[:authorizing_new] diff --git a/spec/lib/email_updater_spec.rb b/spec/lib/email_updater_spec.rb index 0c8b4c6531..a7afd76979 100644 --- a/spec/lib/email_updater_spec.rb +++ b/spec/lib/email_updater_spec.rb @@ -43,6 +43,21 @@ RSpec.describe EmailUpdater do end end + it "sends an email to confirm old email first if require_change_email_confirmation is enabled" do + SiteSetting.require_change_email_confirmation = true + + expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do + updater.change_to(new_email) + end + + expect(updater.change_req).to be_present + expect(updater.change_req.old_email).to eq(old_email) + expect(updater.change_req.new_email).to eq(new_email) + expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old]) + expect(updater.change_req.old_email_token.email).to eq(old_email) + expect(updater.change_req.new_email_token).to be_blank + end + it "logs the admin user as the requester" do updater.change_to(new_email) expect(updater.change_req.requested_by).to eq(admin) From 9daa6328b525069556de8e9a293f197fbf62fe7f Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Fri, 30 Sep 2022 11:01:40 +1000 Subject: [PATCH 003/332] FIX: Quirks around starting new uploads when one was in progress (#18393) This commit addresses issues around starting new uploads in a composer etc. when one or more uploads are already processing or uploading. There were a couple of issues: 1. When all preprocessors were complete, we were not resetting `completeProcessing` to 0, which meant that `needProcessing` would never match `completeProcessing` if a new upload was started. 2. We were relying on the uppy "complete" event which is supposed to fire when all uploads are complete, but this doesn't seem to take into account new uploads that are added. Instead now we can rely on our own `inProgressUploads` tracker, and consider all uploads complete when there are no `inProgressUploads` in flight --- .../app/mixins/composer-upload-uppy.js | 17 ++- .../app/mixins/extendable-uploader.js | 2 + .../discourse/app/mixins/uppy-upload.js | 27 +++-- .../acceptance/composer-uploads-uppy-test.js | 111 ++++++++++++++---- 4 files changed, 115 insertions(+), 42 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 8d8f7c9678..d87ea6a46a 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -339,20 +339,18 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { file.name, upload ); + + if (this.inProgressUploads.length === 0) { + this.appEvents.trigger( + `${this.composerEventPrefix}:all-uploads-complete` + ); + this._reset(); + } }); }); this._uppyInstance.on("upload-error", this._handleUploadError); - this._uppyInstance.on("complete", () => { - run(() => { - this.appEvents.trigger( - `${this.composerEventPrefix}:all-uploads-complete` - ); - this._reset(); - }); - }); - this._uppyInstance.on("cancel-all", () => { // uppyInstance.reset() also fires cancel-all, so we want to // only do the manual cancelling work if the user clicked cancel @@ -553,6 +551,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { isUploading: false, isProcessingUpload: false, isCancellable: false, + inProgressUploads: [], }); this._resetPreProcessors(); this.fileInputEl.value = ""; diff --git a/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js b/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js index ca2a312d3b..8a20a5858b 100644 --- a/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js +++ b/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js @@ -139,6 +139,7 @@ export default Mixin.create(UploadDebugging, { _addNeedProcessing(fileCount) { this._eachPreProcessor((pluginName, status) => { status.needProcessing += fileCount; + status.allComplete = false; }); }, @@ -167,6 +168,7 @@ export default Mixin.create(UploadDebugging, { ) { preProcessorStatus.allComplete = true; preProcessorStatus.needProcessing = 0; + preProcessorStatus.completeProcessing = 0; if (this._allPreprocessorsComplete()) { callback(true); diff --git a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js index 9428e4aea8..11f849bf12 100644 --- a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js +++ b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js @@ -221,6 +221,9 @@ export default Mixin.create(UppyS3Multipart, ExtendableUploader, { ); this._triggerInProgressUploadsEvent(); + if (this.inProgressUploads.length === 0) { + this._allUploadsComplete(); + } }) .catch((errResponse) => { displayErrorForUpload(errResponse, this.siteSettings, file.name); @@ -235,7 +238,11 @@ export default Mixin.create(UppyS3Multipart, ExtendableUploader, { upload ); this.uploadDone(deepMerge(upload, { file_name: file.name })); + this._triggerInProgressUploadsEvent(); + if (this.inProgressUploads.length === 0) { + this._allUploadsComplete(); + } } }); @@ -260,17 +267,6 @@ export default Mixin.create(UppyS3Multipart, ExtendableUploader, { }); }); - this._uppyInstance.on("complete", () => { - run(() => { - if (this.isDestroying || this.isDestroyed) { - return; - } - - this.appEvents.trigger(`upload-mixin:${this.id}:all-uploads-complete`); - this._reset(); - }); - }); - // TODO (martin) preventDirectS3Uploads is necessary because some of // the current upload mixin components, for example the emoji uploader, // send the upload to custom endpoints that do fancy things in the rails @@ -485,4 +481,13 @@ export default Mixin.create(UppyS3Multipart, ExtendableUploader, { _uploadDropTargetOptions() { return { target: this.element }; }, + + _allUploadsComplete() { + if (this.isDestroying || this.isDestroyed) { + return; + } + + this.appEvents.trigger(`upload-mixin:${this.id}:all-uploads-complete`); + this._reset(); + }, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js index 0ff8883870..2f62ab4cad 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js @@ -13,6 +13,8 @@ import { skip, test } from "qunit"; import { Promise } from "rsvp"; import sinon from "sinon"; +let uploadNumber = 1; + function pretender(server, helper) { server.post("/uploads/lookup-urls", () => { return helper.response([ @@ -21,27 +23,53 @@ function pretender(server, helper) { short_path: "/uploads/short-url/yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", short_url: "upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", }, + { + url: "/images/discourse-logo-sketch-small.png", + short_path: "/uploads/short-url/sdfljsdfgjlkwg4328.jpeg", + short_url: "upload://sdfljsdfgjlkwg4328.jpeg", + }, ]); }); server.post( "/uploads.json", () => { - return helper.response({ - extension: "jpeg", - filesize: 126177, - height: 800, - human_filesize: "123 KB", - id: 202, - original_filename: "avatar.PNG.jpg", - retain_hours: null, - short_path: "/uploads/short-url/yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", - short_url: "upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", - thumbnail_height: 320, - thumbnail_width: 690, - url: "/images/discourse-logo-sketch-small.png", - width: 1920, - }); + let response = null; + if (uploadNumber === 1) { + response = { + extension: "jpeg", + filesize: 126177, + height: 800, + human_filesize: "123 KB", + id: 202, + original_filename: "avatar.PNG.jpg", + retain_hours: null, + short_path: "/uploads/short-url/yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", + short_url: "upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", + thumbnail_height: 320, + thumbnail_width: 690, + url: "/images/discourse-logo-sketch-small.png", + width: 1920, + }; + uploadNumber += 1; + } else { + response = { + extension: "jpeg", + filesize: 4322, + height: 800, + human_filesize: "566 KB", + id: 202, + original_filename: "avatar2.PNG.jpg", + retain_hours: null, + short_path: "/uploads/short-url/sdfljsdfgjlkwg4328.jpeg", + short_url: "upload://sdfljsdfgjlkwg4328.jpeg", + thumbnail_height: 320, + thumbnail_width: 690, + url: "/images/discourse-logo-sketch-small.png", + width: 1920, + }; + } + return helper.response(response); }, 500 // this delay is important to slow down the uploads a bit so we can click elements in the UI like the cancel button ); @@ -54,6 +82,9 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { simultaneous_uploads: 2, enable_rich_text_paste: true, }); + needs.hooks.afterEach(() => { + uploadNumber = 1; + }); test("should insert the Uploading placeholder then the complete image placeholder", async function (assert) { await visit("/"); @@ -62,7 +93,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { const appEvents = loggedInUser().appEvents; const done = assert.async(); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" @@ -81,6 +113,35 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { appEvents.trigger("composer:add-files", image); }); + test("should handle adding one file for upload then adding another when the first is still in progress", async function (assert) { + await visit("/"); + await click("#create-topic"); + await fillIn(".d-editor-input", "The image:\n"); + const appEvents = loggedInUser().appEvents; + const done = assert.async(); + + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); + assert.strictEqual( + query(".d-editor-input").value, + "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n![avatar2.PNG|690x320](upload://sdfljsdfgjlkwg4328.jpeg)\n" + ); + done(); + }); + + let image2Added = false; + appEvents.on("composer:upload-started", () => { + if (!image2Added) { + appEvents.trigger("composer:add-files", image2); + image2Added = true; + } + }); + + const image1 = createFile("avatar.png"); + const image2 = createFile("avatar2.png"); + appEvents.trigger("composer:add-files", image1); + }); + test("should handle placeholders correctly even if the OS rewrites ellipses", async function (assert) { const execCommand = document.execCommand; sinon.stub(document, "execCommand").callsFake(function (...args) { @@ -96,7 +157,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { const appEvents = loggedInUser().appEvents; const done = assert.async(); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" @@ -221,7 +283,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); }); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" @@ -251,7 +314,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); }); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n Text after the image." @@ -284,7 +348,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); }); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n Text after the image." @@ -309,7 +374,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); }); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" @@ -335,7 +401,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); }); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" From 4c5e575c15292232c58ce44e2d1566c6f433dc81 Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Fri, 30 Sep 2022 10:02:51 +0800 Subject: [PATCH 004/332] FIX: Ensure closing sidebar tears down all callbacks. (#18434) --- .../sidebar/common/community-section.js | 9 +++++++- .../sidebar/user/categories-section.js | 2 ++ .../sidebar/user/community-section.js | 2 ++ .../sidebar-user-community-section-test.js | 23 ++++++------------- .../tests/acceptance/sidebar-user-test.js | 22 ++++++++++++++++++ 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/sidebar/common/community-section.js b/app/assets/javascripts/discourse/app/components/sidebar/common/community-section.js index 9a2e5174d8..b40754fcbc 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/common/community-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/common/community-section.js @@ -35,7 +35,14 @@ export default class SidebarCommunitySection extends Component { } willDestroy() { - this.sectionLinks.forEach((sectionLink) => sectionLink.teardown()); + [ + ...this.sectionLinks, + ...this.moreSectionLinks, + ...this.moreSecondarySectionLinks, + ].forEach((sectionLink) => { + sectionLink.teardown?.(); + }); + this.topicTrackingState.offStateChange(this.callbackId); } diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/categories-section.js b/app/assets/javascripts/discourse/app/components/sidebar/user/categories-section.js index aee1619f04..4e0d3c3e7c 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/user/categories-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/user/categories-section.js @@ -21,6 +21,8 @@ export default class SidebarUserCategoriesSection extends SidebarCommonCategorie } willDestroy() { + super.willDestroy(...arguments); + this.topicTrackingState.offStateChange(this.callbackId); } diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js b/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js index c3b0fe911a..8f2ffef367 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js @@ -39,6 +39,8 @@ export default class SidebarUserCommunitySection extends SidebarCommonCommunityS } willDestroy() { + super.willDestroy(...arguments); + this.appEvents.off( "user-reviewable-count:changed", this._refreshSectionLinks diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js index f034d089f4..e56736f273 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js @@ -984,6 +984,8 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) { }); test("adding section link via plugin API with callback function", async function (assert) { + let teardownCalled = false; + withPluginApi("1.2.0", (api) => { api.addCommunitySectionLink((baseSectionLink) => { return class CustomSectionLink extends baseSectionLink { @@ -1006,6 +1008,10 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) { get text() { return "my summary"; } + + get teardown() { + teardownCalled = true; + } }; }); }); @@ -1035,24 +1041,9 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) { "eviltrout summary", "displays the right title for the link" ); - }); - - test("clean up topic tracking state state changed callbacks when section is destroyed", async function (assert) { - await visit("/"); - - const topicTrackingState = this.container.lookup( - "service:topic-tracking-state" - ); - - const initialCallbackCount = Object.keys( - topicTrackingState.stateChangeCallbacks - ).length; await click(".btn-sidebar-toggle"); - assert.ok( - Object.keys(topicTrackingState.stateChangeCallbacks).length < - initialCallbackCount - ); + assert.ok(teardownCalled, "section link teardown callback was called"); }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js index 5f5aa93fca..10ed04ae0f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js @@ -169,5 +169,27 @@ acceptance( "displays the mobile icon for the button" ); }); + + test("clean up topic tracking state state changed callbacks when sidebar is destroyed", async function (assert) { + this.siteSettings.tagging_enabled = true; + + await visit("/"); + + const topicTrackingState = this.container.lookup( + "service:topic-tracking-state" + ); + + const initialCallbackCount = Object.keys( + topicTrackingState.stateChangeCallbacks + ).length; + + await click(".btn-sidebar-toggle"); + + assert.strictEqual( + Object.keys(topicTrackingState.stateChangeCallbacks).length, + initialCallbackCount - 3, + "the 3 topic tracking state change callbacks are removed" + ); + }); } ); From 5f81f5d392b1ea767d2fbd2e9f2f482066611831 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:03:11 +0800 Subject: [PATCH 005/332] Build(deps): Bump excon from 0.92.5 to 0.93.0 (#18433) Bumps [excon](https://github.com/excon/excon) from 0.92.5 to 0.93.0. - [Release notes](https://github.com/excon/excon/releases) - [Changelog](https://github.com/excon/excon/blob/master/changelog.txt) - [Commits](https://github.com/excon/excon/compare/v0.92.5...v0.93.0) --- updated-dependencies: - dependency-name: excon dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 1a47d48853..a532e1a9c5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -141,7 +141,7 @@ GEM sprockets (>= 3.3, < 4.1) ember-source (2.18.2) erubi (1.11.0) - excon (0.92.5) + excon (0.93.0) execjs (2.8.1) exifr (1.3.9) fabrication (2.30.0) From ff456510530b6c2dd22af5bbfe8c0dd327311de3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:03:25 +0800 Subject: [PATCH 006/332] Build(deps): Bump net-imap from 0.3.0 to 0.3.1 (#18432) Bumps [net-imap](https://github.com/ruby/net-imap) from 0.3.0 to 0.3.1. - [Release notes](https://github.com/ruby/net-imap/releases) - [Commits](https://github.com/ruby/net-imap/compare/v0.3.0...v0.3.1) --- updated-dependencies: - dependency-name: net-imap dependency-type: indirect update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 a532e1a9c5..8f6fd96267 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -240,7 +240,7 @@ GEM mustache (1.1.1) net-http (0.2.2) uri - net-imap (0.3.0) + net-imap (0.3.1) net-protocol net-pop (0.1.2) net-protocol From 847e1db7fbe9886de7f9059079feadc0395cfe88 Mon Sep 17 00:00:00 2001 From: Meghna <11170663+MeghnaAJ@users.noreply.github.com> Date: Fri, 30 Sep 2022 07:34:54 +0530 Subject: [PATCH 007/332] UX: move dismiss button on the bottom to the right of the footer message (#18424) --- app/assets/stylesheets/desktop/topic-list.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 8587b1b278..e516d09ec0 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -137,6 +137,12 @@ .topic-list-bottom { margin: 20px 0; + .footer-message { + padding-top: 4px; + } + .dismiss-container-bottom { + float: right; + } } // Misc. stuff From 69c20a3a5e418d700ccd7f5b328034d43fff18b7 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 30 Sep 2022 10:35:42 +0800 Subject: [PATCH 008/332] FIX: Removed bookmark reminder alert for reminders set in the past (#18398) --- .../discourse/app/components/bookmark.js | 6 +- .../components/bookmark-alert-test.js | 70 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/discourse/tests/integration/components/bookmark-alert-test.js diff --git a/app/assets/javascripts/discourse/app/components/bookmark.js b/app/assets/javascripts/discourse/app/components/bookmark.js index 0ede10b3c2..14997c5aad 100644 --- a/app/assets/javascripts/discourse/app/components/bookmark.js +++ b/app/assets/javascripts/discourse/app/components/bookmark.js @@ -261,7 +261,11 @@ export default Component.extend({ KeyboardShortcuts.unpause(); }, - showExistingReminderAt: notEmpty("model.reminderAt"), + @discourseComputed("model.reminderAt") + showExistingReminderAt(reminderAt) { + return reminderAt && Date.parse(reminderAt) > new Date().getTime(); + }, + showDelete: notEmpty("model.id"), userHasTimezoneSet: notEmpty("userTimezone"), editingExistingBookmark: and("model", "model.id"), diff --git a/app/assets/javascripts/discourse/tests/integration/components/bookmark-alert-test.js b/app/assets/javascripts/discourse/tests/integration/components/bookmark-alert-test.js new file mode 100644 index 0000000000..f90e5415ab --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/bookmark-alert-test.js @@ -0,0 +1,70 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import { render } from "@ember/test-helpers"; +import { exists } from "discourse/tests/helpers/qunit-helpers"; +import { hbs } from "ember-cli-htmlbars"; + +module("Integration | Component | bookmark-alert", function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.setProperties({ + model: {}, + closeModal: () => {}, + afterSave: () => {}, + afterDelete: () => {}, + registerOnCloseHandler: () => {}, + onCloseWithoutSaving: () => {}, + }); + }); + + test("alert exists for reminder in the future", async function (assert) { + let name = "test"; + let futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 10); + + let reminderAt = futureDate.toISOString(); + this.model = { id: 1, name, reminderAt }; + + await render(hbs` + + `); + + assert.ok( + exists(".existing-reminder-at-alert"), + "alert found for future reminder" + ); + }); + + test("alert does not exist for reminder in the past", async function (assert) { + let name = "test"; + let pastDate = new Date(); + pastDate.setDate(pastDate.getDate() - 1); + + let reminderAt = pastDate.toISOString(); + this.model = { id: 1, name, reminderAt }; + + await render(hbs` + + `); + + assert.ok( + !exists(".existing-reminder-at-alert"), + "alert not found for past reminder" + ); + }); +}); From 079450c9e496ac9bed8ff491ceb1e4ac0b83448f Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Fri, 30 Sep 2022 06:10:07 +0300 Subject: [PATCH 009/332] DEV: Do not show handled reviewables in the user menu (#18402) Currently, the reviewables tab in the user menu shows pending reviewables at the top of the menu and fills the remaining space in the menu with old/handled reviewables. This PR makes the revieables tab show only pending reviewables and hides the tab altogether from the menu if there are no pending reviewables. We're going to follow-up with another change soon that will show pending reviewables in the main tab of the user menu. Internal topic: t/73220. --- .../app/components/user-menu/menu.js | 4 +- .../tests/acceptance/user-menu-test.js | 9 ++- .../components/user-menu/menu-test.js | 11 ++- app/controllers/reviewables_controller.rb | 2 +- app/models/reviewable.rb | 22 ----- spec/models/reviewable_spec.rb | 81 ------------------- spec/requests/reviewables_controller_spec.rb | 32 ++++---- 7 files changed, 37 insertions(+), 124 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/user-menu/menu.js b/app/assets/javascripts/discourse/app/components/user-menu/menu.js index d94c3a941c..403737a898 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/menu.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/menu.js @@ -218,7 +218,9 @@ const CORE_TOP_TABS = [ } get shouldDisplay() { - return this.currentUser.can_review; + return ( + this.currentUser.can_review && this.currentUser.get("reviewable_count") + ); } get count() { diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-menu-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-menu-test.js index da24b409fd..a6fba10a5e 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-menu-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-menu-test.js @@ -119,6 +119,7 @@ acceptance("User menu", function (needs) { }); test("clicking on user menu items", async function (assert) { + updateCurrentUser({ reviewable_count: 1 }); await visit("/"); await click(".d-header-icons .current-user"); await click("#user-menu-button-review-queue"); @@ -169,6 +170,7 @@ acceptance("User menu", function (needs) { }); test("tabs have title attributes", async function (assert) { + updateCurrentUser({ reviewable_count: 1 }); withPluginApi("0.1", (api) => { api.registerUserMenuTab((UserMenuTab) => { return class extends UserMenuTab { @@ -208,7 +210,10 @@ acceptance("User menu", function (needs) { "user-menu-button-messages": I18n.t("user_menu.tabs.messages"), "user-menu-button-bookmarks": I18n.t("user_menu.tabs.bookmarks"), "user-menu-button-tiny-tab-1": "Custom title: 73", - "user-menu-button-review-queue": I18n.t("user_menu.tabs.review_queue"), + "user-menu-button-review-queue": I18n.t( + "user_menu.tabs.review_queue_with_unread", + { count: 1 } + ), "user-menu-button-other-notifications": I18n.t( "user_menu.tabs.other_notifications" ), @@ -235,6 +240,7 @@ acceptance("User menu", function (needs) { }); test("tabs added via the plugin API", async function (assert) { + updateCurrentUser({ reviewable_count: 1 }); withPluginApi("0.1", (api) => { api.registerUserMenuTab((UserMenuTab) => { return class extends UserMenuTab { @@ -674,6 +680,7 @@ acceptance("User menu", function (needs) { }); test("the active tab can be clicked again to navigate to a page", async function (assert) { + updateCurrentUser({ reviewable_count: 1 }); withPluginApi("0.1", (api) => { api.registerUserMenuTab((UserMenuTab) => { return class extends UserMenuTab { diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-test.js index 3687efe765..58b348297b 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-test.js @@ -82,8 +82,9 @@ module("Integration | Component | user-menu", function (hooks) { ); }); - test("reviewables tab is shown if current user can review", async function (assert) { + test("reviewables tab is shown if current user can review and there are pending reviewables", async function (assert) { this.currentUser.set("can_review", true); + this.currentUser.set("reviewable_count", 1); await render(template); const tab = query("#user-menu-button-review-queue"); assert.strictEqual(tab.dataset.tabNumber, "7"); @@ -98,6 +99,13 @@ module("Integration | Component | user-menu", function (hooks) { ); }); + test("reviewables tab is not shown if current user can review but there are no pending reviewables", async function (assert) { + this.currentUser.set("can_review", true); + this.currentUser.set("reviewable_count", 0); + await render(template); + assert.notOk(exists("#user-menu-button-review-queue")); + }); + test("messages tab isn't shown if current user isn't staff and user does not belong to personal_message_enabled_groups", async function (assert) { this.currentUser.set("moderator", false); this.currentUser.set("admin", false); @@ -146,6 +154,7 @@ module("Integration | Component | user-menu", function (hooks) { test("changing tabs", async function (assert) { this.currentUser.set("can_review", true); + this.currentUser.set("reviewable_count", 1); await render(template); let queryParams; pretender.get("/notifications", (request) => { diff --git a/app/controllers/reviewables_controller.rb b/app/controllers/reviewables_controller.rb index 7099796ce0..06d87927e4 100644 --- a/app/controllers/reviewables_controller.rb +++ b/app/controllers/reviewables_controller.rb @@ -71,7 +71,7 @@ class ReviewablesController < ApplicationController end def user_menu_list - reviewables = Reviewable.recent_list_with_pending_first(current_user).to_a + reviewables = Reviewable.list_for(current_user, limit: 30, status: :pending).to_a json = { reviewables: reviewables.map! { |r| r.basic_serializer.new(r, scope: guardian, root: nil).as_json } } diff --git a/app/models/reviewable.rb b/app/models/reviewable.rb index 0f544519ea..b8f57b2b1f 100644 --- a/app/models/reviewable.rb +++ b/app/models/reviewable.rb @@ -534,28 +534,6 @@ class Reviewable < ActiveRecord::Base results end - def self.recent_list_with_pending_first(user, limit: 30) - min_score = Reviewable.min_score_for_priority - - query = Reviewable - .includes(:created_by, :topic, :target) - .viewable_by(user, preload: false) - .except(:order) - .order(score: :desc, created_at: :desc) - .limit(limit) - - if min_score > 0 - query = query.where(<<~SQL, min_score: min_score) - reviewables.score >= :min_score OR reviewables.force_review - SQL - end - records = query.where(status: Reviewable.statuses[:pending]).to_a - if records.size < limit - records += query.where.not(status: Reviewable.statuses[:pending]).to_a - end - records - end - def serializer self.class.serializer_for(self) end diff --git a/spec/models/reviewable_spec.rb b/spec/models/reviewable_spec.rb index 9c1d63d012..53a6bcd252 100644 --- a/spec/models/reviewable_spec.rb +++ b/spec/models/reviewable_spec.rb @@ -300,87 +300,6 @@ RSpec.describe Reviewable, type: :model do end end - describe ".recent_list_with_pending_first" do - fab!(:pending_reviewable1) do - Fabricate( - :reviewable, - score: 150, - created_at: 7.minutes.ago, - status: Reviewable.statuses[:pending] - ) - end - fab!(:pending_reviewable2) do - Fabricate( - :reviewable, - score: 100, - status: Reviewable.statuses[:pending] - ) - end - fab!(:approved_reviewable1) do - Fabricate( - :reviewable, - created_at: 1.minutes.ago, - score: 300, - status: Reviewable.statuses[:approved] - ) - end - fab!(:approved_reviewable2) do - Fabricate( - :reviewable, - created_at: 5.minutes.ago, - score: 200, - status: Reviewable.statuses[:approved] - ) - end - - fab!(:admin) { Fabricate(:admin) } - - it "returns a list of reviewables with pending items first" do - list = Reviewable.recent_list_with_pending_first(admin) - expect(list.map(&:id)).to eq([ - pending_reviewable1.id, - pending_reviewable2.id, - approved_reviewable1.id, - approved_reviewable2.id - ]) - - pending_reviewable1.update!(status: Reviewable.statuses[:rejected]) - rejected_reviewable = pending_reviewable1 - - list = Reviewable.recent_list_with_pending_first(admin) - expect(list.map(&:id)).to eq([ - pending_reviewable2.id, - approved_reviewable1.id, - approved_reviewable2.id, - rejected_reviewable.id, - ]) - end - - it "only includes reviewables whose score is above the minimum or are forced for review" do - SiteSetting.reviewable_default_visibility = 'high' - Reviewable.set_priorities({ high: 200 }) - - list = Reviewable.recent_list_with_pending_first(admin) - expect(list.map(&:id)).to eq([ - approved_reviewable1.id, - approved_reviewable2.id, - ]) - - pending_reviewable1.update!(force_review: true) - - list = Reviewable.recent_list_with_pending_first(admin) - expect(list.map(&:id)).to eq([ - pending_reviewable1.id, - approved_reviewable1.id, - approved_reviewable2.id, - ]) - end - - it "accepts a limit argument to limit the number of returned records" do - expect(Reviewable.recent_list_with_pending_first(admin, limit: 2).size).to eq(2) - end - end - it "valid_types returns the appropriate types" do expect(Reviewable.valid_type?('ReviewableUser')).to eq(true) expect(Reviewable.valid_type?('ReviewableQueuedPost')).to eq(true) diff --git a/spec/requests/reviewables_controller_spec.rb b/spec/requests/reviewables_controller_spec.rb index 3a19ce3ecc..ae84f5e23f 100644 --- a/spec/requests/reviewables_controller_spec.rb +++ b/spec/requests/reviewables_controller_spec.rb @@ -255,7 +255,7 @@ RSpec.describe ReviewablesController do end describe "#user_menu_list" do - it "renders each reviewable with its basic serializers" do + it "renders each reviewable using its basic serializers" do reviewable_user = Fabricate(:reviewable_user, payload: { username: "someb0dy" }) reviewable_flagged_post = Fabricate(:reviewable_flagged_post) reviewable_queued_post = Fabricate(:reviewable_queued_post) @@ -284,40 +284,38 @@ RSpec.describe ReviewablesController do end it "returns JSON containing basic information of reviewables" do - reviewable1 = Fabricate(:reviewable) - reviewable2 = Fabricate(:reviewable, status: Reviewable.statuses[:approved]) + reviewable = Fabricate(:reviewable) get "/review/user-menu-list.json" expect(response.status).to eq(200) reviewables = response.parsed_body["reviewables"] - expect(reviewables.size).to eq(2) - expect(reviewables[0]["flagger_username"]).to eq(reviewable1.created_by.username) - expect(reviewables[0]["id"]).to eq(reviewable1.id) - expect(reviewables[0]["type"]).to eq(reviewable1.type) + expect(reviewables.size).to eq(1) + expect(reviewables[0]["flagger_username"]).to eq(reviewable.created_by.username) + expect(reviewables[0]["id"]).to eq(reviewable.id) + expect(reviewables[0]["type"]).to eq(reviewable.type) expect(reviewables[0]["pending"]).to eq(true) - - expect(reviewables[1]["flagger_username"]).to eq(reviewable2.created_by.username) - expect(reviewables[1]["id"]).to eq(reviewable2.id) - expect(reviewables[1]["type"]).to eq(reviewable2.type) - expect(reviewables[1]["pending"]).to eq(false) end - it "puts pending reviewables on top" do - approved1 = Fabricate( + it "responds with pending reviewables only" do + Fabricate( :reviewable, status: Reviewable.statuses[:approved] ) - pending = Fabricate( + pending1 = Fabricate( :reviewable, status: Reviewable.statuses[:pending] ) - approved2 = Fabricate( + Fabricate( :reviewable, status: Reviewable.statuses[:approved] ) + pending2 = Fabricate( + :reviewable, + status: Reviewable.statuses[:pending] + ) get "/review/user-menu-list.json" expect(response.status).to eq(200) reviewables = response.parsed_body["reviewables"] - expect(reviewables.map { |r| r["id"] }).to eq([pending.id, approved2.id, approved1.id]) + expect(reviewables.map { |r| r["id"] }).to eq([pending2.id, pending1.id]) end end From 0bdb616edc23975fd33a39227781556246cceb35 Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Fri, 30 Sep 2022 13:13:50 +0800 Subject: [PATCH 010/332] DEV: Refactor community section code (#18436) In a recent commit when adding the review section link, I moved to a pattern where we allowed the section links to be refreshed after the section has been constructed. However, we were not tearing down the old section links when refreshing. This made me realise that refreshing section links in a section is not a pattern I want to adopt since people can easily forget to teardown. Instead, each section link should be responsible for defining a teardown function for cleanup which will always be called when the sidebar is removed. --- .../sidebar/common/community-section.hbs | 1 + .../sidebar/common/community-section.js | 51 ++++--- .../components/sidebar/more-section-link.hbs | 1 + .../app/components/sidebar/section-link.hbs | 126 +++++++++--------- .../app/components/sidebar/section-link.js | 8 ++ .../sidebar/user/community-section.js | 42 ++---- .../sidebar/base-community-section-link.js | 2 + .../community-section/review-section-link.js | 34 ++++- 8 files changed, 140 insertions(+), 125 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/sidebar/common/community-section.hbs b/app/assets/javascripts/discourse/app/components/sidebar/common/community-section.hbs index b7b54e3173..2727a2093b 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/common/community-section.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/common/community-section.hbs @@ -13,6 +13,7 @@ {{#each this.sectionLinks as |sectionLink|}} { this.sectionLinks.forEach((sectionLink) => { @@ -61,41 +77,20 @@ export default class SidebarCommunitySection extends Component { return []; } - refreshSectionLinks() { - this.moreSectionLinks = this.#initializeSectionLinks([ - ...this.defaultMoreSectionLinks, - ...customSectionLinks, - ]); - - this.moreSecondarySectionLinks = this.#initializeSectionLinks([ - ...this.defaultMoreSecondarySectionLinks, - ...secondaryCustomSectionLinks, - ]); - - this.sectionLinks = this.#initializeSectionLinks( - this.defaultMainSectionLinks - ); + #initializeSectionLinks(sectionLinkClasses, { inMoreDrawer } = {}) { + return sectionLinkClasses.map((sectionLinkClass) => { + return this.#initializeSectionLink(sectionLinkClass, inMoreDrawer); + }); } - #initializeSectionLinks(sectionLinkClasses) { - return sectionLinkClasses.reduce((links, sectionLinkClass) => { - const sectionLink = this.#initializeSectionLink(sectionLinkClass); - - if (sectionLink.shouldDisplay) { - links.push(sectionLink); - } - - return links; - }, []); - } - - #initializeSectionLink(sectionLinkClass) { + #initializeSectionLink(sectionLinkClass, inMoreDrawer) { return new sectionLinkClass({ topicTrackingState: this.topicTrackingState, currentUser: this.currentUser, appEvents: this.appEvents, router: this.router, siteSettings: this.siteSettings, + inMoreDrawer, }); } } diff --git a/app/assets/javascripts/discourse/app/components/sidebar/more-section-link.hbs b/app/assets/javascripts/discourse/app/components/sidebar/more-section-link.hbs index 321beab314..408e8abc65 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/more-section-link.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/more-section-link.hbs @@ -1,4 +1,5 @@ - {{#if @href}} - - +{{#if this.shouldDisplay}} + + + {{/if}} + + {{#if @hoverValue}} + + + + {{/if}} + + {{/if}} + +{{/if}} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-link.js b/app/assets/javascripts/discourse/app/components/sidebar/section-link.js index 16385ec1f9..7b87c8f8e6 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section-link.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/section-link.js @@ -8,6 +8,14 @@ export default class SectionLink extends Component { } } + get shouldDisplay() { + if (this.args.shouldDisplay === undefined) { + return true; + } + + return this.args.shouldDisplay; + } + get classNames() { let classNames = [ "sidebar-section-link", diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js b/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js index 8f2ffef367..1191981a20 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js @@ -1,6 +1,5 @@ import I18n from "I18n"; -import { bind } from "discourse-common/utils/decorators"; import Composer from "discourse/models/composer"; import { getOwner } from "discourse-common/lib/get-owner"; import PermissionType from "discourse/models/permission-type"; @@ -31,50 +30,25 @@ export default class SidebarUserCommunitySection extends SidebarCommonCommunityS title: I18n.t("sidebar.sections.community.header_action_title"), }, ]; - - this.appEvents.on( - "user-reviewable-count:changed", - this._refreshSectionLinks - ); - } - - willDestroy() { - super.willDestroy(...arguments); - - this.appEvents.off( - "user-reviewable-count:changed", - this._refreshSectionLinks - ); - } - - @bind - _refreshSectionLinks() { - return this.refreshSectionLinks(); } get defaultMainSectionLinks() { - const links = [ + return [ EverythingSectionLink, TrackedSectionLink, MyPostsSectionLink, AdminSectionLink, + ReviewSectionLink, ]; - - if (this.currentUser.reviewable_count > 0) { - links.push(ReviewSectionLink); - } - - return links; } get defaultMoreSectionLinks() { - const links = [GroupsSectionLink, UsersSectionLink, BadgesSectionLink]; - - if (this.currentUser.reviewable_count === 0) { - links.push(ReviewSectionLink); - } - - return links; + return [ + GroupsSectionLink, + UsersSectionLink, + BadgesSectionLink, + ReviewSectionLink, + ]; } get defaultMoreSecondarySectionLinks() { diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/base-community-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/base-community-section-link.js index 0ed22c7390..e7dc1603fd 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/base-community-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/base-community-section-link.js @@ -8,12 +8,14 @@ export default class BaseCommunitySectionLink { appEvents, router, siteSettings, + inMoreDrawer, } = {}) { this.router = router; this.topicTrackingState = topicTrackingState; this.currentUser = currentUser; this.appEvents = appEvents; this.siteSettings = siteSettings; + this.inMoreDrawer = inMoreDrawer; } /** diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js index 52563a0389..04716cca09 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js @@ -1,8 +1,40 @@ import I18n from "I18n"; +import { tracked } from "@glimmer/tracking"; + +import { bind } from "discourse-common/utils/decorators"; import BaseSectionLink from "discourse/lib/sidebar/base-community-section-link"; export default class ReviewSectionLink extends BaseSectionLink { + @tracked canDisplay; + + constructor() { + super(...arguments); + + this._refreshCanDisplay(); + this.appEvents.on("user-reviewable-count:changed", this._refreshCanDisplay); + } + + teardown() { + this.appEvents.off( + "user-reviewable-count:changed", + this._refreshCanDisplay + ); + } + + @bind + _refreshCanDisplay() { + if (!this.currentUser.can_review) { + this.canDisplay = false; + } + + if (this.inMoreDrawer) { + this.canDisplay = this.currentUser.reviewable_count < 1; + } else { + this.canDisplay = this.currentUser.reviewable_count > 0; + } + } + get name() { return "review"; } @@ -20,7 +52,7 @@ export default class ReviewSectionLink extends BaseSectionLink { } get shouldDisplay() { - return this.currentUser.can_review; + return this.canDisplay; } get badgeText() { From 6ebd2cecda6330cc40bd70d6336a44ad65f22b3f Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 30 Sep 2022 10:48:26 +0530 Subject: [PATCH 011/332] FIX: missing theme upload should not break precompile process. (#18431) Previously, if an active default theme's upload record went missing then it will break the site and cause downtime. --- lib/stylesheet/manager/builder.rb | 2 +- spec/lib/stylesheet/manager_spec.rb | 31 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/stylesheet/manager/builder.rb b/lib/stylesheet/manager/builder.rb index 4a0ed806c1..137d48db9f 100644 --- a/lib/stylesheet/manager/builder.rb +++ b/lib/stylesheet/manager/builder.rb @@ -222,7 +222,7 @@ class Stylesheet::Manager::Builder sha1s = [] (theme&.upload_fields || []).map do |upload_field| - sha1s << upload_field.upload.sha1 + sha1s << upload_field.upload&.sha1 end Digest::SHA1.hexdigest(sha1s.sort!.join("\n")) diff --git a/spec/lib/stylesheet/manager_spec.rb b/spec/lib/stylesheet/manager_spec.rb index 9a24f9cb33..218f2c4938 100644 --- a/spec/lib/stylesheet/manager_spec.rb +++ b/spec/lib/stylesheet/manager_spec.rb @@ -351,6 +351,37 @@ RSpec.describe Stylesheet::Manager do expect(digest1).not_to eq(digest2) end + it 'can generate digest with a missing upload record' do + theme = Fabricate(:theme) + + upload = UploadCreator.new(image, "logo.png").create_for(-1) + field = ThemeField.create!( + theme_id: theme.id, + target_id: Theme.targets[:common], + name: "logo", + value: "", + upload_id: upload.id, + type_id: ThemeField.types[:theme_upload_var] + ) + + manager = manager(theme.id) + + builder = Stylesheet::Manager::Builder.new( + target: :desktop_theme, theme: theme, manager: manager + ) + + digest1 = builder.digest + upload.delete + + builder = Stylesheet::Manager::Builder.new( + target: :desktop_theme, theme: theme.reload, manager: manager + ) + + digest2 = builder.digest + + expect(digest1).not_to eq(digest2) + end + it 'returns different digest based on target' do theme = Fabricate(:theme) builder = Stylesheet::Manager::Builder.new(target: :desktop_theme, theme: theme, manager: manager) From 5a5625460bd9ac3e156c7fa26739ef073143bdd0 Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Fri, 30 Sep 2022 08:44:04 +0300 Subject: [PATCH 012/332] DEV: Add group messages and group_message_summary notifications in the messages tab in the user menu (#18390) This commit adds non-archived group messages and `group_message_summary` notifications in the messages tab in the user menu. With this change, the messages tab in the user menu now includes 3 types of items: 1. Unread `private_message` notifications (notifications when you receive a reply in a PM) 2. Unread and read `group_message_summary` notifications (notifications when there's a new message in a group inbox that you track) 3. Non-archived personal and group messages Unread `private_message` notifications are always shown first, followed by unread `group_message_summary` notifications, and then everything else (messages and read `group_message_summary` notifications) sorted by recency (most recent first). Internal topic: t/72976. --- .../app/components/user-menu/menu.js | 2 +- .../app/components/user-menu/messages-list.js | 56 ++++- .../tests/acceptance/user-menu-test.js | 4 +- .../discourse/tests/fixtures/user-menu.js | 147 +++++------ .../user-menu/messages-list-test.js | 233 +++++++++++++++++- app/controllers/users_controller.rb | 61 +++-- app/models/notification.rb | 23 +- lib/topic_query/private_message_lists.rb | 30 ++- spec/fabricators/post_fabricator.rb | 19 ++ spec/requests/users_controller_spec.rb | 119 +++++++-- 10 files changed, 554 insertions(+), 140 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/user-menu/menu.js b/app/assets/javascripts/discourse/app/components/user-menu/menu.js index 403737a898..ff21b008a0 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/menu.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/menu.js @@ -170,7 +170,7 @@ const CORE_TOP_TABS = [ } get notificationTypes() { - return ["private_message"]; + return ["private_message", "group_message_summary"]; } get linkWhenActive() { diff --git a/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js b/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js index ac434948c0..7d916ec668 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js @@ -7,9 +7,21 @@ import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item" import UserMenuMessageItem from "discourse/lib/user-menu/message-item"; import Topic from "discourse/models/topic"; +function parseDateString(date) { + if (date) { + return new Date(date); + } +} + +async function initializeNotifications(rawList) { + const notifications = rawList.map((n) => Notification.create(n)); + await Notification.applyTransformations(notifications); + return notifications; +} + export default class UserMenuMessagesList extends UserMenuNotificationsList { get dismissTypes() { - return ["private_message"]; + return this.filterByTypes; } get showAllHref() { @@ -51,9 +63,10 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList { ); const content = []; - const notifications = data.notifications.map((n) => Notification.create(n)); - await Notification.applyTransformations(notifications); - notifications.forEach((notification) => { + const unreadNotifications = await initializeNotifications( + data.unread_notifications + ); + unreadNotifications.forEach((notification) => { content.push( new UserMenuNotificationItem({ notification, @@ -66,12 +79,39 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList { const topics = data.topics.map((t) => Topic.create(t)); await Topic.applyTransformations(topics); - content.push( - ...topics.map((topic) => { - return new UserMenuMessageItem({ message: topic }); - }) + + const readNotifications = await initializeNotifications( + data.read_notifications ); + let latestReadNotificationDate = parseDateString( + readNotifications[0]?.created_at + ); + let latestMessageDate = parseDateString(topics[0]?.bumped_at); + + while (latestReadNotificationDate || latestMessageDate) { + if ( + !latestReadNotificationDate || + (latestMessageDate && latestReadNotificationDate < latestMessageDate) + ) { + content.push(new UserMenuMessageItem({ message: topics[0] })); + topics.shift(); + latestMessageDate = parseDateString(topics[0]?.bumped_at); + } else { + content.push( + new UserMenuNotificationItem({ + notification: readNotifications[0], + currentUser: this.currentUser, + siteSettings: this.siteSettings, + site: this.site, + }) + ); + readNotifications.shift(); + latestReadNotificationDate = parseDateString( + readNotifications[0]?.created_at + ); + } + } return content; } diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-menu-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-menu-test.js index a6fba10a5e..09f9a135f7 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-menu-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-menu-test.js @@ -805,7 +805,7 @@ acceptance("User menu - Dismiss button", function (needs) { const copy = cloneJSON( UserMenuFixtures["/u/:username/user-menu-private-messages"] ); - copy.notifications = []; + copy.unread_notifications = []; return helper.response(copy); } else { return helper.response( @@ -941,7 +941,7 @@ acceptance("User menu - Dismiss button", function (needs) { assert.ok(markRead, "mark-read request is sent"); assert.strictEqual( markReadRequestBody, - "dismiss_types=private_message", + "dismiss_types=private_message%2Cgroup_message_summary", "mark-read request specifies private_message types" ); assert.notOk(exists(".user-menu .notifications-dismiss")); diff --git a/app/assets/javascripts/discourse/tests/fixtures/user-menu.js b/app/assets/javascripts/discourse/tests/fixtures/user-menu.js index a645cce16a..63b8bdc4b9 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/user-menu.js +++ b/app/assets/javascripts/discourse/tests/fixtures/user-menu.js @@ -64,79 +64,80 @@ export default { ], }, "/u/:username/user-menu-private-messages": { - notifications: [ - { - id: 8315, - user_id: 1, - notification_type: 6, - read: false, - high_priority: true, - created_at: "2022-08-05T17:27:24.873Z", - post_number: 1, - topic_id: 249, - fancy_title: "Very secret message!", - slug: "very-secret-message", - data: { - topic_title: "very secret message!", - original_post_id: 1043, - original_post_type: 1, - original_username: "osama", - revision_number: null, - display_username: "osama" - }, + unread_notifications: [ + { + id: 8315, + user_id: 1, + notification_type: 6, + read: false, + high_priority: true, + created_at: "2022-08-05T17:27:24.873Z", + post_number: 1, + topic_id: 249, + fancy_title: "Very secret message!", + slug: "very-secret-message", + data: { + topic_title: "very secret message!", + original_post_id: 1043, + original_post_type: 1, + original_username: "osama", + revision_number: null, + display_username: "osama" }, - ], - topics: [ - { - id: 8092, - title: "BUG: Can not render emoji properly :/", - fancy_title: "BUG: Can not render emoji properly :confused:", - slug: "bug-can-not-render-emoji-properly", - posts_count: 1, - reply_count: 0, - highest_post_number: 2, - image_url: null, - created_at: "2019-07-26T01:29:24.008Z", - last_posted_at: "2019-07-26T01:29:24.177Z", - bumped: true, - bumped_at: "2019-07-26T01:29:24.177Z", - unseen: false, - last_read_post_number: 2, - unread_posts: 0, - pinned: false, - unpinned: null, - visible: true, - closed: false, - archived: false, - notification_level: 3, - bookmarked: false, - bookmarks: [], - liked: false, - views: 5, - like_count: 0, - has_summary: false, - archetype: "private_message", - last_poster_username: "mixtape", - category_id: null, - pinned_globally: false, - featured_link: null, - posters: [ - { - extras: "latest single", - description: "Original Poster, Most Recent Poster", - user_id: 13, - primary_group_id: null, - }, - ], - participants: [ - { - extras: "latest", - description: null, - user_id: 13, - primary_group_id: null, - }, - ], - } - ], + }, + ], + topics: [ + { + id: 8092, + title: "BUG: Can not render emoji properly :/", + fancy_title: "BUG: Can not render emoji properly :confused:", + slug: "bug-can-not-render-emoji-properly", + posts_count: 1, + reply_count: 0, + highest_post_number: 2, + image_url: null, + created_at: "2019-07-26T01:29:24.008Z", + last_posted_at: "2019-07-26T01:29:24.177Z", + bumped: true, + bumped_at: "2019-07-26T01:29:24.177Z", + unseen: false, + last_read_post_number: 2, + unread_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 3, + bookmarked: false, + bookmarks: [], + liked: false, + views: 5, + like_count: 0, + has_summary: false, + archetype: "private_message", + last_poster_username: "mixtape", + category_id: null, + pinned_globally: false, + featured_link: null, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user_id: 13, + primary_group_id: null, + }, + ], + participants: [ + { + extras: "latest", + description: null, + user_id: 13, + primary_group_id: null, + }, + ], + } + ], + read_notifications: [], } } diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/messages-list-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/messages-list-test.js index 02fba00dc7..49de60fb0c 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/messages-list-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/user-menu/messages-list-test.js @@ -5,14 +5,128 @@ import { render, settled } from "@ember/test-helpers"; import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types"; import { hbs } from "ember-cli-htmlbars"; import pretender, { response } from "discourse/tests/helpers/create-pretender"; +import { cloneJSON, deepMerge } from "discourse-common/lib/object"; +import UserMenuFixtures from "discourse/tests/fixtures/user-menu"; import I18n from "I18n"; +function getMessage(overrides = {}) { + return deepMerge( + { + id: 8092, + title: "Test ToPic 4422", + fancy_title: "Test topic 4422", + slug: "test-topic-4422", + posts_count: 1, + reply_count: 0, + highest_post_number: 2, + image_url: null, + created_at: "2019-07-26T01:29:24.008Z", + last_posted_at: "2019-07-26T01:29:24.177Z", + bumped: true, + bumped_at: "2019-07-26T01:29:24.177Z", + unseen: false, + last_read_post_number: 2, + unread_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 3, + bookmarked: false, + bookmarks: [], + liked: false, + views: 5, + like_count: 0, + has_summary: false, + archetype: "private_message", + last_poster_username: "mixtape", + category_id: null, + pinned_globally: false, + featured_link: null, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user_id: 13, + primary_group_id: null, + }, + ], + participants: [ + { + extras: "latest", + description: null, + user_id: 13, + primary_group_id: null, + }, + ], + }, + overrides + ); +} + +function getGroupMessageSummaryNotification(overrides = {}) { + return deepMerge( + { + id: 9492, + user_id: 1, + notification_type: 16, + read: true, + high_priority: false, + created_at: "2022-08-05T17:27:24.873Z", + post_number: null, + topic_id: null, + fancy_title: null, + slug: null, + data: { + group_id: 1, + group_name: "jokers", + inbox_count: 4, + username: "joker.leader", + }, + }, + overrides + ); +} + module("Integration | Component | user-menu | messages-list", function (hooks) { setupRenderingTest(hooks); const template = hbs``; - test("renders notifications on top and messages on bottom", async function (assert) { + test("renders unread PM notifications first followed by messages and read group_message_summary notifications", async function (assert) { + pretender.get("/u/eviltrout/user-menu-private-messages", () => { + const copy = cloneJSON( + UserMenuFixtures["/u/:username/user-menu-private-messages"] + ); + copy.read_notifications = [getGroupMessageSummaryNotification()]; + return response(copy); + }); + await render(template); + const items = queryAll("ul li"); + + assert.strictEqual(items.length, 3); + + assert.ok(items[0].classList.contains("notification")); + assert.ok(items[0].classList.contains("unread")); + assert.ok(items[0].classList.contains("private-message")); + + assert.ok(items[1].classList.contains("notification")); + assert.ok(items[1].classList.contains("read")); + assert.ok(items[1].classList.contains("group-message-summary")); + + assert.ok(items[2].classList.contains("message")); + }); + + test("does not error when there are no group_message_summary notifications", async function (assert) { + pretender.get("/u/eviltrout/user-menu-private-messages", () => { + const copy = cloneJSON( + UserMenuFixtures["/u/:username/user-menu-private-messages"] + ); + copy.read_notifications = []; + return response(copy); + }); + await render(template); const items = queryAll("ul li"); @@ -25,6 +139,117 @@ module("Integration | Component | user-menu | messages-list", function (hooks) { assert.ok(items[1].classList.contains("message")); }); + test("does not error when there are no messages", async function (assert) { + pretender.get("/u/eviltrout/user-menu-private-messages", () => { + const copy = cloneJSON( + UserMenuFixtures["/u/:username/user-menu-private-messages"] + ); + copy.topics = []; + copy.read_notifications = [getGroupMessageSummaryNotification()]; + return response(copy); + }); + + await render(template); + const items = queryAll("ul li"); + + assert.strictEqual(items.length, 2); + + assert.ok(items[0].classList.contains("notification")); + assert.ok(items[0].classList.contains("unread")); + assert.ok(items[0].classList.contains("private-message")); + + assert.ok(items[1].classList.contains("notification")); + assert.ok(items[1].classList.contains("read")); + assert.ok(items[1].classList.contains("group-message-summary")); + }); + + test("merge-sorts group_message_summary notifications and messages", async function (assert) { + pretender.get("/u/eviltrout/user-menu-private-messages", () => { + const copy = cloneJSON( + UserMenuFixtures["/u/:username/user-menu-private-messages"] + ); + copy.unread_notifications = []; + copy.topics = [ + getMessage({ + bumped_at: "2014-07-26T01:29:24.177Z", + fancy_title: "Test Topic 0003", + }), + getMessage({ + bumped_at: "2012-07-26T01:29:24.177Z", + fancy_title: "Test Topic 0002", + }), + getMessage({ + bumped_at: "2010-07-26T01:29:24.177Z", + fancy_title: "Test Topic 0001", + }), + ]; + copy.read_notifications = [ + getGroupMessageSummaryNotification({ + created_at: "2015-07-26T01:29:24.177Z", + data: { + inbox_count: 13, + }, + }), + getGroupMessageSummaryNotification({ + created_at: "2013-07-26T01:29:24.177Z", + data: { + inbox_count: 12, + }, + }), + getGroupMessageSummaryNotification({ + created_at: "2011-07-26T01:29:24.177Z", + data: { + inbox_count: 11, + }, + }), + ]; + return response(copy); + }); + await render(template); + const items = queryAll("ul li"); + + assert.strictEqual(items.length, 6); + + assert.strictEqual( + items[0].textContent.trim(), + I18n.t("notifications.group_message_summary", { + count: 13, + group_name: "jokers", + }) + ); + + assert.strictEqual( + items[1].textContent.trim().replaceAll(/\s+/g, " "), + "mixtape Test Topic 0003" + ); + + assert.strictEqual( + items[2].textContent.trim(), + I18n.t("notifications.group_message_summary", { + count: 12, + group_name: "jokers", + }) + ); + + assert.strictEqual( + items[3].textContent.trim().replaceAll(/\s+/g, " "), + "mixtape Test Topic 0002" + ); + + assert.strictEqual( + items[4].textContent.trim(), + I18n.t("notifications.group_message_summary", { + count: 11, + group_name: "jokers", + }) + ); + + assert.strictEqual( + items[5].textContent.trim().replaceAll(/\s+/g, " "), + "mixtape Test Topic 0001" + ); + }); + test("show all link", async function (assert) { await render(template); const link = query(".panel-body-bottom .show-all"); @@ -66,7 +291,11 @@ module("Integration | Component | user-menu | messages-list", function (hooks) { test("empty state (aka blank page syndrome)", async function (assert) { pretender.get("/u/eviltrout/user-menu-private-messages", () => { - return response({ notifications: [], topics: [] }); + return response({ + unread_notifications: [], + topics: [], + read_notifications: [], + }); }); await render(template); assert.strictEqual( diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 9367dd73ff..255da241f9 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1746,11 +1746,10 @@ class UsersController < ApplicationController raise Discourse::InvalidAccess.new("username doesn't match current_user's username") end - reminder_notifications = Notification.unread_type( - current_user, - Notification.types[:bookmark_reminder], - USER_MENU_LIST_LIMIT - ) + reminder_notifications = Notification + .for_user_menu(current_user.id, limit: USER_MENU_LIST_LIMIT) + .unread + .where(notification_type: Notification.types[:bookmark_reminder]) if reminder_notifications.size < USER_MENU_LIST_LIMIT exclude_bookmark_ids = reminder_notifications @@ -1803,29 +1802,37 @@ class UsersController < ApplicationController raise Discourse::InvalidAccess.new("personal messages are disabled.") end - message_notifications = Notification.unread_type( - current_user, - Notification.types[:private_message], - USER_MENU_LIST_LIMIT - ) + unread_notifications = Notification + .for_user_menu(current_user.id, limit: USER_MENU_LIST_LIMIT) + .unread + .where(notification_type: [Notification.types[:private_message], Notification.types[:group_message_summary]]) + .to_a - if message_notifications.size < USER_MENU_LIST_LIMIT - exclude_topic_ids = message_notifications.map(&:topic_id).uniq + if unread_notifications.size < USER_MENU_LIST_LIMIT + exclude_topic_ids = unread_notifications.filter_map(&:topic_id).uniq + limit = USER_MENU_LIST_LIMIT - unread_notifications.size messages_list = TopicQuery.new( current_user, - per_page: USER_MENU_LIST_LIMIT - message_notifications.size - ).list_private_messages(current_user) do |query| + per_page: limit + ).list_private_messages_direct_and_groups(current_user) do |query| if exclude_topic_ids.present? query.where("topics.id NOT IN (?)", exclude_topic_ids) else query end end + read_notifications = Notification + .for_user_menu(current_user.id, limit: limit) + .where( + read: true, + notification_type: Notification.types[:group_message_summary], + ) + .to_a end - if message_notifications.present? - serialized_notifications = ActiveModel::ArraySerializer.new( - message_notifications, + if unread_notifications.present? + serialized_unread_notifications = ActiveModel::ArraySerializer.new( + unread_notifications, each_serializer: NotificationSerializer, scope: guardian ) @@ -1840,8 +1847,17 @@ class UsersController < ApplicationController )[:topics] end + if read_notifications.present? + serialized_read_notifications = ActiveModel::ArraySerializer.new( + read_notifications, + each_serializer: NotificationSerializer, + scope: guardian + ) + end + render json: { - notifications: serialized_notifications || [], + unread_notifications: serialized_unread_notifications || [], + read_notifications: serialized_read_notifications || [], topics: serialized_messages || [] } end @@ -2027,13 +2043,4 @@ class UsersController < ApplicationController { users: ActiveModel::ArraySerializer.new(users, each_serializer: each_serializer).as_json } end - - def find_unread_notifications_of_type(type, limit) - current_user - .notifications - .visible - .includes(:topic) - .where(read: false, notification_type: type) - .limit(limit) - end end diff --git a/app/models/notification.rb b/app/models/notification.rb index fc4e8ffe69..3c72cc2f1d 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -15,14 +15,26 @@ class Notification < ActiveRecord::Base scope :recent, lambda { |n = nil| n ||= 10; order('notifications.created_at desc').limit(n) } scope :visible , lambda { joins('LEFT JOIN topics ON notifications.topic_id = topics.id') .where('topics.id IS NULL OR topics.deleted_at IS NULL') } - scope :unread_type, ->(user, type, limit = 20) do - where(user_id: user.id, read: false, notification_type: type).visible.includes(:topic).limit(limit) + scope :unread_type, ->(user, type, limit = 30) do + unread_types(user, [type], limit) end - scope :prioritized, ->(limit = nil) do + scope :unread_types, ->(user, types, limit = 30) do + where(user_id: user.id, read: false, notification_type: types) + .visible + .includes(:topic) + .limit(limit) + end + scope :prioritized, ->() do order("notifications.high_priority AND NOT notifications.read DESC") .order("NOT notifications.read DESC") .order("notifications.created_at DESC") - .limit(limit || 30) + end + scope :for_user_menu, ->(user_id, limit: 30) do + where(user_id: user_id) + .visible + .prioritized + .includes(:topic) + .limit(limit) end attr_accessor :skip_send_email @@ -226,7 +238,8 @@ class Notification < ActiveRecord::Base notifications = user.notifications .includes(:topic) .visible - .prioritized(count) + .prioritized + .limit(count) if types.present? notifications = notifications.where(notification_type: types) diff --git a/lib/topic_query/private_message_lists.rb b/lib/topic_query/private_message_lists.rb index cdf5201b02..395b54fad5 100644 --- a/lib/topic_query/private_message_lists.rb +++ b/lib/topic_query/private_message_lists.rb @@ -5,14 +5,16 @@ class TopicQuery def list_private_messages(user, &blk) list = private_messages_for(user, :user) list = not_archived(list, user) + list = have_posts_from_others(list, user) - list = list.where(<<~SQL) - NOT ( - topics.participant_count = 1 - AND topics.user_id = #{user.id.to_i} - AND topics.moderator_posts_count = 0 - ) - SQL + create_list(:private_messages, {}, list, &blk) + end + + def list_private_messages_direct_and_groups(user, &blk) + list = private_messages_for(user, :all) + list = not_archived(list, user) + list = not_archived_in_groups(list) + list = have_posts_from_others(list, user) create_list(:private_messages, {}, list, &blk) end @@ -252,6 +254,20 @@ class TopicQuery .where('um.user_id IS NULL') end + def not_archived_in_groups(list) + list.left_joins(:group_archived_messages).where(group_archived_messages: { id: nil }) + end + + def have_posts_from_others(list, user) + list.where(<<~SQL) + NOT ( + topics.participant_count = 1 + AND topics.user_id = #{user.id.to_i} + AND topics.moderator_posts_count = 0 + ) + SQL + end + def group @group ||= begin Group diff --git a/spec/fabricators/post_fabricator.rb b/spec/fabricators/post_fabricator.rb index 4b35bc721c..e18628a141 100644 --- a/spec/fabricators/post_fabricator.rb +++ b/spec/fabricators/post_fabricator.rb @@ -143,6 +143,25 @@ Fabricator(:private_message_post, from: :post) do raw "Ssshh! This is our secret conversation!" end +Fabricator(:group_private_message_post, from: :post) do + transient :recipients + user + topic do |attrs| + Fabricate(:private_message_topic, + user: attrs[:user], + created_at: attrs[:created_at], + subtype: TopicSubtype.user_to_user, + topic_allowed_users: [ + Fabricate.build(:topic_allowed_user, user: attrs[:user]), + ], + topic_allowed_groups: [ + Fabricate.build(:topic_allowed_group, group: attrs[:recipients] || Fabricate(:group)) + ] + ) + end + raw "Ssshh! This is our group secret conversation!" +end + Fabricator(:private_message_post_one_user, from: :post) do user topic do |attrs| diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index ff74b1f41f..ebcf3a0c56 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -5797,9 +5797,21 @@ RSpec.describe UsersController do end describe "#user_menu_messages" do + fab!(:group1) { Fabricate(:group, has_messages: true, users: [user]) } + fab!(:group2) { Fabricate(:group, has_messages: true, users: [user, user1]) } + fab!(:group3) { Fabricate(:group, has_messages: true, users: [user1]) } + fab!(:message_without_notification) { Fabricate(:private_message_post, recipient: user).topic } fab!(:message_with_read_notification) { Fabricate(:private_message_post, recipient: user).topic } fab!(:message_with_unread_notification) { Fabricate(:private_message_post, recipient: user).topic } + fab!(:archived_message) { Fabricate(:private_message_post, recipient: user).topic } + + fab!(:group_message1) { Fabricate(:group_private_message_post, recipients: group1).topic } + fab!(:group_message2) { Fabricate(:group_private_message_post, recipients: group2).topic } + fab!(:group_message3) { Fabricate(:group_private_message_post, recipients: group3).topic } + + fab!(:archived_group_message1) { Fabricate(:group_private_message_post, recipients: group1).topic } + fab!(:archived_group_message2) { Fabricate(:group_private_message_post, recipients: group2).topic } fab!(:user1_message_without_notification) do Fabricate(:private_message_post, recipient: user1).topic @@ -5810,16 +5822,18 @@ RSpec.describe UsersController do fab!(:user1_message_with_unread_notification) do Fabricate(:private_message_post, recipient: user1).topic end + fab!(:user1_archived_message) { Fabricate(:private_message_post, recipient: user1).topic } - fab!(:unread_notification) do + fab!(:unread_pm_notification) do Fabricate( :private_message_notification, read: false, user: user, - topic: message_with_unread_notification + topic: message_with_unread_notification, + created_at: 4.minutes.ago ) end - fab!(:read_notification) do + fab!(:read_pm_notification) do Fabricate( :private_message_notification, read: true, @@ -5828,7 +5842,27 @@ RSpec.describe UsersController do ) end - fab!(:user1_unread_notification) do + fab!(:unread_group_message_summary_notification) do + Fabricate( + :notification, + read: false, + user: user, + notification_type: Notification.types[:group_message_summary], + created_at: 2.minutes.ago + ) + end + + fab!(:read_group_message_summary_notification) do + Fabricate( + :notification, + read: true, + user: user, + notification_type: Notification.types[:group_message_summary], + created_at: 1.minutes.ago + ) + end + + fab!(:user1_unread_pm_notification) do Fabricate( :private_message_notification, read: false, @@ -5836,7 +5870,7 @@ RSpec.describe UsersController do topic: user1_message_with_unread_notification ) end - fab!(:user1_read_notification) do + fab!(:user1_read_pm_notification) do Fabricate( :private_message_notification, read: true, @@ -5845,6 +5879,30 @@ RSpec.describe UsersController do ) end + fab!(:user1_unread_group_message_summary_notification) do + Fabricate( + :notification, + read: false, + user: user1, + notification_type: Notification.types[:group_message_summary], + ) + end + fab!(:user1_read_group_message_summary_notification) do + Fabricate( + :notification, + read: true, + user: user1, + notification_type: Notification.types[:group_message_summary], + ) + end + + before do + UserArchivedMessage.archive!(user.id, archived_message) + UserArchivedMessage.archive!(user1.id, user1_archived_message) + GroupArchivedMessage.archive!(group1.id, archived_group_message1) + GroupArchivedMessage.archive!(group2.id, archived_group_message2) + end + context "when logged out" do it "responds with 404" do get "/u/#{user.username}/user-menu-private-messages" @@ -5873,33 +5931,62 @@ RSpec.describe UsersController do get "/u/#{user.username}/user-menu-private-messages" expect(response.status).to eq(200) - notifications = response.parsed_body["notifications"] - expect(notifications.map { |notification| notification["id"] }).to contain_exactly( - unread_notification.id + unread_notifications = response.parsed_body["unread_notifications"] + expect(unread_notifications.map { |notification| notification["id"] }).to eq([ + unread_pm_notification.id, + unread_group_message_summary_notification.id + ]) + end + + it "sends an array of read group_message_summary notifications" do + read_group_message_summary_notification2 = Fabricate( + :notification, + read: true, + user: user, + notification_type: Notification.types[:group_message_summary], + created_at: 5.minutes.ago ) + get "/u/#{user.username}/user-menu-private-messages" + expect(response.status).to eq(200) + + read_notifications = response.parsed_body["read_notifications"] + expect(read_notifications.map { |notification| notification["id"] }).to eq([ + read_group_message_summary_notification.id, + read_group_message_summary_notification2.id + ]) end it "responds with an array of PM topics that are not associated with any of the unread private_message notifications" do + group_message1.update!(bumped_at: 1.minutes.ago) + message_without_notification.update!(bumped_at: 3.minutes.ago) + group_message2.update!(bumped_at: 6.minutes.ago) + message_with_read_notification.update!(bumped_at: 10.minutes.ago) + read_group_message_summary_notification.destroy! + get "/u/#{user.username}/user-menu-private-messages" expect(response.status).to eq(200) topics = response.parsed_body["topics"] - expect(topics.map { |topic| topic["id"] }).to contain_exactly( + expect(topics.map { |topic| topic["id"] }).to eq([ + group_message1.id, message_without_notification.id, + group_message2.id, message_with_read_notification.id - ) + ]) end it "fills up the remaining of the USER_MENU_LIST_LIMIT limit with PM topics" do - stub_const(UsersController, "USER_MENU_LIST_LIMIT", 2) do + stub_const(UsersController, "USER_MENU_LIST_LIMIT", 3) do get "/u/#{user.username}/user-menu-private-messages" end expect(response.status).to eq(200) - notifications = response.parsed_body["notifications"] - expect(notifications.size).to eq(1) + unread_notifications = response.parsed_body["unread_notifications"] + expect(unread_notifications.size).to eq(2) topics = response.parsed_body["topics"] + read_notifications = response.parsed_body["read_notifications"] expect(topics.size).to eq(1) + expect(read_notifications.size).to eq(1) message2 = Fabricate(:private_message_post, recipient: user).topic Fabricate( @@ -5913,11 +6000,13 @@ RSpec.describe UsersController do get "/u/#{user.username}/user-menu-private-messages" end expect(response.status).to eq(200) - notifications = response.parsed_body["notifications"] - expect(notifications.size).to eq(2) + unread_notifications = response.parsed_body["unread_notifications"] + expect(unread_notifications.size).to eq(2) topics = response.parsed_body["topics"] + read_notifications = response.parsed_body["read_notifications"] expect(topics.size).to eq(0) + expect(read_notifications.size).to eq(0) end end end From 58cc35fc780926a2a706077e573fd9bd4b655669 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Sep 2022 09:00:18 +0200 Subject: [PATCH 013/332] Build(deps-dev): Bump webdrivers from 5.1.0 to 5.2.0 (#18435) Bumps [webdrivers](https://github.com/titusfortner/webdrivers) from 5.1.0 to 5.2.0. - [Release notes](https://github.com/titusfortner/webdrivers/releases) - [Changelog](https://github.com/titusfortner/webdrivers/blob/main/CHANGELOG.md) - [Commits](https://github.com/titusfortner/webdrivers/compare/v5.1.0...v5.2.0) --- updated-dependencies: - dependency-name: webdrivers dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 8f6fd96267..82ff3123e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -486,7 +486,7 @@ GEM uri (0.11.0) uri_template (0.7.0) version_gem (1.1.0) - webdrivers (5.1.0) + webdrivers (5.2.0) nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (~> 4.0) From 0c38757250e12304518d654b4efcc7db741b82c1 Mon Sep 17 00:00:00 2001 From: Selase Krakani <849886+s3lase@users.noreply.github.com> Date: Fri, 30 Sep 2022 08:28:09 +0000 Subject: [PATCH 014/332] FIX: Revert recursively tag lookup with missing ancestor tags (#18439) This reverts commit 049f8569d89264dd630e4eeb8625b44735c1ae92. To be revisited with a more comprehensive solution covering parent selection when multiple parents exist. --- lib/discourse_tagging.rb | 32 +++++------------- spec/lib/discourse_tagging_spec.rb | 53 ------------------------------ 2 files changed, 8 insertions(+), 77 deletions(-) diff --git a/lib/discourse_tagging.rb b/lib/discourse_tagging.rb index e3832fd0e1..8e64bab2a5 100644 --- a/lib/discourse_tagging.rb +++ b/lib/discourse_tagging.rb @@ -13,29 +13,6 @@ module DiscourseTagging ON tgm.tag_group_id = tg.id SQL - ANCESTOR_TAG_IDS_SQL ||= <<~SQL - WITH RECURSIVE ancestors AS ( - SELECT - tgm.tag_id, - tg.parent_tag_id - FROM - tag_group_memberships tgm - INNER JOIN tag_groups tg ON tg.id = tgm.tag_group_id - WHERE - tg.parent_tag_id IS NOT NULL - AND tgm.tag_id IN (:tag_ids) - UNION - SELECT - tgm.tag_id, - tg.parent_tag_id - FROM - tag_group_memberships tgm - INNER JOIN tag_groups tg ON tg.id = tgm.tag_group_id - INNER JOIN ancestors ON tgm.tag_id = ancestors.parent_tag_id - ) - SELECT * FROM ancestors - SQL - def self.tag_topic_by_names(topic, guardian, tag_names_arg, append: false) if guardian.can_tag?(topic) tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, guardian) || [] @@ -116,7 +93,14 @@ module DiscourseTagging # add missing mandatory parent tags tag_ids = tags.map(&:id) - parent_tags_map = DB.query(ANCESTOR_TAG_IDS_SQL, tag_ids: tag_ids).inject({}) do |h, v| + parent_tags_map = DB.query(" + SELECT tgm.tag_id, tg.parent_tag_id + FROM tag_groups tg + INNER JOIN tag_group_memberships tgm + ON tgm.tag_group_id = tg.id + WHERE tg.parent_tag_id IS NOT NULL + AND tgm.tag_id IN (?) + ", tag_ids).inject({}) do |h, v| h[v.tag_id] ||= [] h[v.tag_id] << v.parent_tag_id h diff --git a/spec/lib/discourse_tagging_spec.rb b/spec/lib/discourse_tagging_spec.rb index 548489c31b..ddcd88916d 100644 --- a/spec/lib/discourse_tagging_spec.rb +++ b/spec/lib/discourse_tagging_spec.rb @@ -497,19 +497,9 @@ RSpec.describe DiscourseTagging do end it "adds all parent tags that are missing" do - parent_tag_group = Fabricate(:tag_group) parent_tag = Fabricate(:tag, name: 'parent') - parent_tag_group.tags = [parent_tag] - tag_group2 = Fabricate(:tag_group, parent_tag_id: parent_tag.id) tag_group2.tags = [tag2] - - tag_group1 = Fabricate(:tag_group) - tag_group1.tags = [tag1] - - tag_group3 = Fabricate(:tag_group, parent_tag_id: tag1.id) - tag_group3.tags = [tag3] - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag3.name, tag2.name]) expect(valid).to eq(true) expect(topic.reload.tags.map(&:name)).to contain_exactly( @@ -529,49 +519,6 @@ RSpec.describe DiscourseTagging do expect(valid).to eq(true) expect(topic.reload.tags.map(&:name)).to contain_exactly(*[parent_tag, common].map(&:name)) end - - context "when tag group has grandparents" do - let(:tag_group1) { Fabricate(:tag_group) } - let(:foo) { Fabricate(:tag, name: 'foo') } - let(:tag_group2) { Fabricate(:tag_group, parent_tag_id: foo.id) } - let(:bar) { Fabricate(:tag, name: 'bar') } - let(:tag_group3) { Fabricate(:tag_group, parent_tag_id: bar.id) } - let(:baz) { Fabricate(:tag, name: 'baz') } - - before do - tag_group1.tags = [foo] - tag_group2.tags = [bar] - tag_group3.tags = [baz] - end - - it "recursively adds all ancestors" do - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [baz.name]) - expect(valid).to eq(true) - expect(topic.reload.tags.map(&:name)).to contain_exactly(*[foo, bar, baz].map(&:name)) - end - - it "adds only one parent when multiple parents exist" do - alt_parent_group = Fabricate(:tag_group) - alt = Fabricate(:tag, name: 'alt') - alt_parent_group.tags = [alt] - - alt_baz_group = Fabricate(:tag_group, parent_tag_id: alt.id) - alt_baz_group.tags = [baz] - - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [baz.name]) - expect(valid).to eq(true) - expect(topic.reload.tags.map(&:name)).to contain_exactly(*[foo, bar, baz].map(&:name)) - end - - it "adds missing tags even with cycles" do - tag_group4 = Fabricate(:tag_group, parent_tag_id: baz.id) - tag_group4.tags = [foo] - - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [baz.name]) - expect(valid).to eq(true) - expect(topic.reload.tags.map(&:name)).to contain_exactly(*[foo, bar, baz].map(&:name)) - end - end end context "when enforcing required tags from a tag group" do From 35a90b6a3fca11af7957f146af1c5db4352dd666 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Fri, 30 Sep 2022 13:35:00 +0300 Subject: [PATCH 015/332] FIX: Add better and more strict invite validators (#18399) * FIX: Add validator for email xor domain * FIX: Add validator for max_redemptions_allowed * FIX: Add validator for redemption_count --- app/models/invite.rb | 18 +++++++++++++++++- config/locales/server.en.yml | 3 +++ spec/models/invite_spec.rb | 27 ++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/app/models/invite.rb b/app/models/invite.rb index b0cc7e38b7..4b0c92620e 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -31,8 +31,10 @@ class Invite < ActiveRecord::Base validates_presence_of :invited_by_id validates :email, email: true, allow_blank: true validate :ensure_max_redemptions_allowed + validate :valid_redemption_count validate :valid_domain, if: :will_save_change_to_domain? validate :user_doesnt_already_exist, if: :will_save_change_to_email? + validate :email_xor_domain before_create do self.invite_key ||= SecureRandom.base58(10) @@ -66,6 +68,12 @@ class Invite < ActiveRecord::Base end end + def email_xor_domain + if email.present? && domain.present? + errors.add(:base, I18n.t('invite.email_xor_domain')) + end + end + def is_invite_link? email.blank? end @@ -253,12 +261,20 @@ class Invite < ActiveRecord::Base limit = invited_by&.staff? ? SiteSetting.invite_link_max_redemptions_limit : SiteSetting.invite_link_max_redemptions_limit_users - if !self.max_redemptions_allowed.between?(1, limit) + if self.email.present? && self.max_redemptions_allowed != 1 + errors.add(:max_redemptions_allowed, I18n.t("invite.max_redemptions_allowed_one")) + elsif !self.max_redemptions_allowed.between?(1, limit) errors.add(:max_redemptions_allowed, I18n.t("invite_link.max_redemptions_limit", max_limit: limit)) end end end + def valid_redemption_count + if self.redemption_count > self.max_redemptions_allowed + errors.add(:redemption_count, I18n.t("invite.redemption_count_less_than_max", max_redemptions_allowed: self.max_redemptions_allowed)) + end + end + def valid_domain return if self.domain.blank? diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 251474c7f5..1d08c5e04a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -264,6 +264,9 @@ en: invalid_access: "You are not permitted to view the requested resource." requires_groups: "Invite was not saved because the specified topic is inaccessible. Add one of the following groups: %{groups}." domain_not_allowed: "Your email cannot be used to redeem this invite." + max_redemptions_allowed_one: "for email invites should be 1." + redemption_count_less_than_max: "should be less than %{max_redemptions_allowed}." + email_xor_domain: "Email and domain fields are not allowed at the same time" bulk_invite: file_should_be_csv: "The uploaded file should be of csv format." diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index faabf6229d..2691659db3 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -41,12 +41,33 @@ RSpec.describe Invite do end it 'allows only valid domains' do - invite = Fabricate.build(:invite, domain: 'example', invited_by: user) + invite = Fabricate.build(:invite, email: nil, domain: 'example', invited_by: user) expect(invite).not_to be_valid - invite = Fabricate.build(:invite, domain: 'example.com', invited_by: user) + invite = Fabricate.build(:invite, email: nil, domain: 'example.com', invited_by: user) expect(invite).to be_valid end + + it 'allows only email or only domain to be present' do + invite = Fabricate.build(:invite, email: nil, invited_by: user) + expect(invite).to be_valid + + invite = Fabricate.build(:invite, email: nil, domain: 'example.com', invited_by: user) + expect(invite).to be_valid + + invite = Fabricate.build(:invite, email: 'test@example.com', invited_by: user) + expect(invite).to be_valid + + invite = Fabricate.build(:invite, email: 'test@example.com', domain: 'example.com', invited_by: user) + expect(invite).not_to be_valid + expect(invite.errors.full_messages).to include(I18n.t('invite.email_xor_domain')) + end + + it 'checks if redemption_count is less or equal than max_redemptions_allowed' do + invite = Fabricate.build(:invite, redemption_count: 2, max_redemptions_allowed: 1, invited_by: user) + expect(invite).not_to be_valid + expect(invite.errors.full_messages.first).to include(I18n.t('invite.redemption_count_less_than_max', max_redemptions_allowed: 1)) + end end describe 'before_save' do @@ -355,7 +376,7 @@ RSpec.describe Invite do fab!(:inviter) { Fabricate(:user) } fab!(:pending_invite) { Fabricate(:invite, invited_by: inviter, email: 'pending@example.com') } - fab!(:pending_link_invite) { Fabricate(:invite, invited_by: inviter, max_redemptions_allowed: 5) } + fab!(:pending_link_invite) { Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5) } fab!(:pending_invite_from_another_user) { Fabricate(:invite) } fab!(:expired_invite) { Fabricate(:invite, invited_by: inviter, email: 'expired@example.com', expires_at: 1.day.ago) } From b615201b886121c30547632acd04bf080d120706 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Fri, 30 Sep 2022 18:34:47 +0300 Subject: [PATCH 016/332] FIX: Remove zero-width space when not necessary (#18429) A zero-width space character is inserted for icon-only buttons, but that is unnecessary when the button has some rich-content and the block form is used. --- .../javascripts/discourse/app/templates/components/d-button.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/app/templates/components/d-button.hbs b/app/assets/javascripts/discourse/app/templates/components/d-button.hbs index e6586be8dd..a05a03a390 100644 --- a/app/assets/javascripts/discourse/app/templates/components/d-button.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/d-button.hbs @@ -8,7 +8,7 @@ {{~#if this.computedLabel~}} {{html-safe this.computedLabel}}{{#if this.ellipsis}}…{{/if}} -{{~else~}} +{{~else if (not (has-block))~}} ​ {{! Zero-width space character, so icon-only button height = regular button height }} {{~/if~}} From 136174e0eec7d5cfaefac657a919c19e587fa7c9 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 30 Sep 2022 21:27:41 +0530 Subject: [PATCH 017/332] FEATURE: when entering a topic scroll to last visited line marker (#18440) When a user enters a topic they have already visited they are navigated to that post that is newest for them (post_number_last_read + 1). Above that post there is a "last visited" line marker which is visible when the user scrolls a bit above the post they landed on. This commit makes sure that the "last visited" line marker is visible as soon as user is landed in the topic. --- app/assets/javascripts/discourse/app/lib/url.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/assets/javascripts/discourse/app/lib/url.js b/app/assets/javascripts/discourse/app/lib/url.js index 622911c417..1d979f4c56 100644 --- a/app/assets/javascripts/discourse/app/lib/url.js +++ b/app/assets/javascripts/discourse/app/lib/url.js @@ -117,6 +117,15 @@ const DiscourseURL = EmberObject.extend({ if (!holder) { selector = holderId; + + if ( + document.getElementsByClassName( + `topic-post-visited-line post-${postNumber - 1}` + )?.length === 1 + ) { + selector = ".small-action.topic-post-visited"; + } + holder = document.querySelector(selector); } From c1a7fa6b5de6692d7b14b28ff89f08e4940f3837 Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Fri, 30 Sep 2022 13:03:20 -0500 Subject: [PATCH 018/332] FIX: Allow logout for admins in staff-writes-only-mode (#18441) --- app/assets/javascripts/discourse/app/routes/application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/app/routes/application.js b/app/assets/javascripts/discourse/app/routes/application.js index a0962b7773..9a9f08e9ef 100644 --- a/app/assets/javascripts/discourse/app/routes/application.js +++ b/app/assets/javascripts/discourse/app/routes/application.js @@ -55,7 +55,7 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, { this.controllerFor("application").send("toggleSidebar"); }, - logout: unlessReadOnly( + logout: unlessStrictlyReadOnly( "_handleLogout", I18n.t("read_only_mode.logout_disabled") ), From 3b869743674e0a7f29b0954c32f00f4bb886a1ed Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Fri, 30 Sep 2022 12:20:21 -0600 Subject: [PATCH 019/332] FEATURE: Make General the default category (#18383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * FEATURE: Make General the default category * Set general as the default category in the composer model instead * use semicolon * Enable allow_uncategorized_topics in create_post spec helper for now * Check if general_category_id is set * Enable allow_uncategorized_topics for test env * Provide an option to the create_post helper to not set allow_uncategorized_topics * Add tests to check that category… is not present and that General is selected automatically --- .../discourse/app/models/composer.js | 4 ++- .../tests/acceptance/composer-test.js | 23 ++++++++++++++++- .../select-kit/category-chooser-test.js | 17 +++++++++++++ .../addon/components/category-chooser.js | 5 +++- config/environments/test.rb | 5 ++++ config/site_settings.yml | 3 ++- ...7_disable_allow_uncategorized_new_sites.rb | 25 +++++++++++++++++++ spec/models/topic_converter_spec.rb | 2 +- spec/support/helpers.rb | 6 +++++ 9 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20220927171707_disable_allow_uncategorized_new_sites.rb diff --git a/app/assets/javascripts/discourse/app/models/composer.js b/app/assets/javascripts/discourse/app/models/composer.js index 2c365f47f2..bf548f633c 100644 --- a/app/assets/javascripts/discourse/app/models/composer.js +++ b/app/assets/javascripts/discourse/app/models/composer.js @@ -143,7 +143,9 @@ const Composer = RestModel.extend({ const oldCategoryId = this._categoryId; if (isEmpty(categoryId)) { - categoryId = null; + // Set General as the default category + const generalCategoryId = this.siteSettings.general_category_id; + categoryId = generalCategoryId ? generalCategoryId : null; } this._categoryId = categoryId; diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js index 88c616e3fd..6fa0cec03e 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js @@ -39,8 +39,27 @@ acceptance("Composer", function (needs) { }); needs.settings({ enable_whispers: true, + general_category_id: 1, + }); + needs.site({ + can_tag_topics: true, + categories: [ + { + id: 1, + name: "General", + slug: "general", + permission: 1, + topic_template: null, + }, + { + id: 2, + name: "test too", + slug: "test-too", + permission: 1, + topic_template: "", + }, + ], }); - needs.site({ can_tag_topics: true }); needs.pretender((server, helper) => { server.post("/uploads/lookup-urls", () => { return helper.response([]); @@ -69,6 +88,8 @@ acceptance("Composer", function (needs) { test("Composer is opened", async function (assert) { await visit("/"); await click("#create-topic"); + // Check that General category is selected + assert.strictEqual(selectKit(".category-chooser").header().value(), "1"); assert.strictEqual( document.documentElement.style.getPropertyValue("--composer-height"), diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js index 426b914d6f..5446f009a8 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js @@ -126,6 +126,23 @@ module( assert.strictEqual(this.subject.header().label(), "category…"); }); + test("with allowUncategorized=null and generalCategoryId present", async function (assert) { + this.siteSettings.allow_uncategorized_topics = false; + this.siteSettings.general_category_id = 4; + + await render(hbs` + + `); + + assert.strictEqual(this.subject.header().value(), null); + assert.strictEqual(this.subject.header().label(), ""); + }); + test("with allowUncategorized=null none=true", async function (assert) { this.siteSettings.allow_uncategorized_topics = false; diff --git a/app/assets/javascripts/select-kit/addon/components/category-chooser.js b/app/assets/javascripts/select-kit/addon/components/category-chooser.js index f2e420a0b4..2345fb2f61 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-chooser.js +++ b/app/assets/javascripts/select-kit/addon/components/category-chooser.js @@ -44,7 +44,10 @@ export default ComboBoxComponent.extend({ ) { return Category.findUncategorized(); } else { - return this.defaultItem(null, htmlSafe(I18n.t("category.choose"))); + const generalCategoryId = this.siteSettings.general_category_id; + if (!generalCategoryId) { + return this.defaultItem(null, htmlSafe(I18n.t("category.choose"))); + } } }, diff --git a/config/environments/test.rb b/config/environments/test.rb index ce5485047f..7a25d5b45c 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -68,6 +68,11 @@ Discourse::Application.configure do s.set_regardless_of_locale(:download_remote_images_to_local, false) s.set_regardless_of_locale(:unique_posts_mins, 0) s.set_regardless_of_locale(:max_consecutive_replies, 0) + + # Most existing tests were written assuming allow_uncategorized_topics + # was enabled, so we should set it to true. + s.set_regardless_of_locale(:allow_uncategorized_topics, true) + # disable plugins if ENV['LOAD_PLUGINS'] == '1' s.set_regardless_of_locale(:discourse_narrative_bot_enabled, false) diff --git a/config/site_settings.yml b/config/site_settings.yml index ebf5c76a6b..8243bd1a7b 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -806,7 +806,7 @@ posting: max_emojis_in_title: 1 allow_uncategorized_topics: client: true - default: true + default: false refresh: true allow_duplicate_topic_titles: false allow_duplicate_topic_titles_category: false @@ -2304,6 +2304,7 @@ uncategorized: general_category_id: default: -1 hidden: true + client: true meta_category_id: default: -1 hidden: true diff --git a/db/migrate/20220927171707_disable_allow_uncategorized_new_sites.rb b/db/migrate/20220927171707_disable_allow_uncategorized_new_sites.rb new file mode 100644 index 0000000000..979b8a6b56 --- /dev/null +++ b/db/migrate/20220927171707_disable_allow_uncategorized_new_sites.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class DisableAllowUncategorizedNewSites < ActiveRecord::Migration[7.0] + def up + result = execute <<~SQL + SELECT created_at + FROM schema_migration_details + ORDER BY created_at + LIMIT 1 + SQL + + # keep allow uncategorized for existing sites + if result.first['created_at'].to_datetime < 1.hour.ago + execute <<~SQL + INSERT INTO site_settings(name, data_type, value, created_at, updated_at) + VALUES('allow_uncategorized_topics', 5, 't', NOW(), NOW()) + ON CONFLICT (name) DO NOTHING + SQL + end + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/spec/models/topic_converter_spec.rb b/spec/models/topic_converter_spec.rb index d66b1cd045..7598b14432 100644 --- a/spec/models/topic_converter_spec.rb +++ b/spec/models/topic_converter_spec.rb @@ -6,7 +6,7 @@ RSpec.describe TopicConverter do fab!(:author) { Fabricate(:user) } fab!(:category) { Fabricate(:category, topic_count: 1) } fab!(:private_message) { Fabricate(:private_message_topic, user: author) } # creates a topic without a first post - let(:first_post) { create_post(user: author, topic: private_message) } + let(:first_post) { create_post(user: author, topic: private_message, allow_uncategorized_topics: false) } let(:other_user) { private_message.topic_allowed_users.find { |u| u.user != author }.user } let(:uncategorized_category) do diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index c92b928185..9ef8c27d4d 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -45,6 +45,12 @@ module Helpers end def create_post(args = {}) + # Pretty much all the tests with `create_post` will fail without this + # since allow_uncategorized_topics is now false by default + unless args[:allow_uncategorized_topics] == false + SiteSetting.allow_uncategorized_topics = true + end + args[:title] ||= "This is my title #{Helpers.next_seq}" args[:raw] ||= "This is the raw body of my post, it is cool #{Helpers.next_seq}" args[:topic_id] = args[:topic].id if args[:topic] From afce65bb79f6d9afd3444bb0d4fc29c8c05a944a Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 30 Sep 2022 14:51:44 -0400 Subject: [PATCH 020/332] UX: fix post placeholder on mobile (#18442) --- app/assets/stylesheets/mobile/topic-post.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 0dabffd02c..ffa4a91436 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -431,3 +431,7 @@ span.highlighted { opacity: 100%; margin-bottom: 1rem; } + +.placeholder .topic-body { + width: 100%; +} From 563ec624b2dcf497323e6b727cda9625fcf7a365 Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Fri, 30 Sep 2022 14:12:49 -0500 Subject: [PATCH 021/332] FIX: Allow email login for admins in staff-writes-only-mode (#18443) --- app/controllers/session_controller.rb | 2 ++ app/controllers/users_controller.rb | 1 + spec/requests/session_controller_spec.rb | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index be2535241f..68d8a367a8 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -11,6 +11,7 @@ class SessionController < ApplicationController requires_login only: [:second_factor_auth_show, :second_factor_auth_perform] allow_in_staff_writes_only_mode :create + allow_in_staff_writes_only_mode :email_login ACTIVATE_USER_KEY = "activate_user" @@ -375,6 +376,7 @@ class SessionController < ApplicationController elsif payload = login_error_check(user) return render json: payload else + raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff? user.update_timezone_if_missing(params[:timezone]) log_on_user(user) return render json: success_json diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 255da241f9..a28716270e 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -52,6 +52,7 @@ class UsersController < ApplicationController after_action :add_noindex_header, only: [:show, :my_redirect] allow_in_staff_writes_only_mode :admin_login + allow_in_staff_writes_only_mode :email_login MAX_RECENT_SEARCHES = 5 diff --git a/spec/requests/session_controller_spec.rb b/spec/requests/session_controller_spec.rb index adebaface7..cadac9ece9 100644 --- a/spec/requests/session_controller_spec.rb +++ b/spec/requests/session_controller_spec.rb @@ -129,6 +129,27 @@ RSpec.describe SessionController do SiteSetting.enable_local_logins_via_email = true end + context "when in staff writes only mode" do + use_redis_snapshotting + + before do + Discourse.enable_readonly_mode(Discourse::STAFF_WRITES_ONLY_MODE_KEY) + end + + it "allows admins to login" do + user.update!(admin: true) + post "/session/email-login/#{email_token.token}.json" + expect(response.status).to eq(200) + expect(session[:current_user_id]).to eq(user.id) + end + + it "does not allow other users to login" do + post "/session/email-login/#{email_token.token}.json" + expect(response.status).to eq(503) + expect(session[:current_user_id]).to eq(nil) + end + end + context "when local logins via email disabled" do before { SiteSetting.enable_local_logins_via_email = false } From ff42bef1b69f9e274745c42aef6318691f228b5d Mon Sep 17 00:00:00 2001 From: jbrw Date: Fri, 30 Sep 2022 17:14:21 -0400 Subject: [PATCH 022/332] DEV: Add new plugin outlet in topic list header (#18444) --- .../javascripts/discourse/app/templates/topic-list-header.hbr | 1 + 1 file changed, 1 insertion(+) 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 cdca90b5c5..7a7c44f30d 100644 --- a/app/assets/javascripts/discourse/app/templates/topic-list-header.hbr +++ b/app/assets/javascripts/discourse/app/templates/topic-list-header.hbr @@ -7,6 +7,7 @@ {{/if}} {{raw "topic-list-header-column" order='default' name=listTitle bulkSelectEnabled=bulkSelectEnabled showBulkToggle=toggleInTitle canBulkSelect=canBulkSelect canDoBulkActions=canDoBulkActions}} +{{raw-plugin-outlet name="topic-list-header-after-main-link"}} {{#if showPosters}} {{raw "topic-list-header-column" order='posters' ariaLabel=(i18n "category.sort_options.posters")}} {{/if}} From a3ce93bb98e5e80d578cb9a67498ca8edec66b55 Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Sun, 2 Oct 2022 22:56:57 +0300 Subject: [PATCH 023/332] FIX: Workaround a bug in the R2 gem to produce valid RTL CSS (#18446) See the comment in the changed file for details. Meta report: https://meta.discourse.org/t/main-css-and-mobile-style-not-working-after-update-2-9-0-beta10/240553?u=osama. --- .../base/sidebar-more-section-links.scss | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/sidebar-more-section-links.scss b/app/assets/stylesheets/common/base/sidebar-more-section-links.scss index 41b698786d..9d237c57c2 100644 --- a/app/assets/stylesheets/common/base/sidebar-more-section-links.scss +++ b/app/assets/stylesheets/common/base/sidebar-more-section-links.scss @@ -44,7 +44,27 @@ margin: 0 calc(var(--d-sidebar-row-horizontal-padding) * 2 / 3); .sidebar-row { - padding: 0.33rem calc(var(--d-sidebar-row-horizontal-padding) / 3); + // the multiplication by 1 here is a workaround for a bug in the R2 gem + // that we use to generate RTL CSS. + // the gem generates RTL CSS by converting anything left to right and + // vice versa. for example, a `padding-right: 1px;` property becomes + // `padding-left: 1px;` when it goes through the gem. + // the gem also handles the `padding` property (and similar properties) + // when it's in the 4-sides form, e.g. `padding: 1px 2px 3px 4px;` which + // gets converted to `padding: 1px 4px 3px 2px;`. + // however, the problem is that the gem detects 4-sides properties pretty + // naively - it splits the property value on /\s+/ and if it has 4 parts, + // it swaps the second and fourth parts. + // if you remove the by 1 multiplication in our rule below, we end up + // with a value that can be split into 4 parts and that causes the R2 gem + // to convert the rule to this: + // padding: 0.33rem 3) / calc(var(--d-sidebar-row-horizontal-padding); + // which is clearly invalid and breaks all the rules that come after this + // one in the application CSS bundle. + // in the long term we should probably find (or write ourselves) + // something that's smarter than R2, but for now let's workaround the bug + // in R2. + padding: 0.33rem calc(1 * var(--d-sidebar-row-horizontal-padding) / 3); } } From 28f8ade0aaef603d61d62f4af3978d9fe26b8b07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Oct 2022 23:26:14 +0200 Subject: [PATCH 024/332] Build(deps): Bump msgpack from 1.5.6 to 1.6.0 (#18451) Bumps [msgpack](https://github.com/msgpack/msgpack-ruby) from 1.5.6 to 1.6.0. - [Release notes](https://github.com/msgpack/msgpack-ruby/releases) - [Changelog](https://github.com/msgpack/msgpack-ruby/blob/master/ChangeLog) - [Commits](https://github.com/msgpack/msgpack-ruby/compare/v1.5.6...v1.6.0) --- updated-dependencies: - dependency-name: msgpack dependency-type: indirect update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 82ff3123e6..1fa10c3b45 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -234,7 +234,7 @@ GEM ffi (~> 1.9) minitest (5.16.3) mocha (1.15.0) - msgpack (1.5.6) + msgpack (1.6.0) multi_json (1.15.0) multi_xml (0.6.0) mustache (1.1.1) From 3f14df479620823275eadc395998d1b5b368e3de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Oct 2022 23:26:35 +0200 Subject: [PATCH 025/332] Build(deps): Bump zeitwerk from 2.6.0 to 2.6.1 (#18450) Bumps [zeitwerk](https://github.com/fxn/zeitwerk) from 2.6.0 to 2.6.1. - [Release notes](https://github.com/fxn/zeitwerk/releases) - [Changelog](https://github.com/fxn/zeitwerk/blob/main/CHANGELOG.md) - [Commits](https://github.com/fxn/zeitwerk/compare/v2.6.0...v2.6.1) --- updated-dependencies: - dependency-name: zeitwerk dependency-type: indirect update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 1fa10c3b45..fc0fd8a697 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -502,7 +502,7 @@ GEM xpath (3.2.0) nokogiri (~> 1.8) yaml-lint (0.0.10) - zeitwerk (2.6.0) + zeitwerk (2.6.1) PLATFORMS aarch64-linux From 6ef4cf195a4fa4c75881c78119249b5a2db2500a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Oct 2022 23:26:54 +0200 Subject: [PATCH 026/332] Build(deps): Bump exifr from 1.3.9 to 1.3.10 (#18449) Bumps [exifr](https://github.com/remvee/exifr) from 1.3.9 to 1.3.10. - [Release notes](https://github.com/remvee/exifr/releases) - [Changelog](https://github.com/remvee/exifr/blob/master/CHANGELOG) - [Commits](https://github.com/remvee/exifr/compare/release-1.3.9...release-1.3.10) --- updated-dependencies: - dependency-name: exifr dependency-type: indirect update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 fc0fd8a697..ff6215699d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -143,7 +143,7 @@ GEM erubi (1.11.0) excon (0.93.0) execjs (2.8.1) - exifr (1.3.9) + exifr (1.3.10) fabrication (2.30.0) faker (2.23.0) i18n (>= 1.8.11, < 2) From 8fe52bbee79f6b684c81ffa9b89302f6682e1974 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Oct 2022 23:27:55 +0200 Subject: [PATCH 027/332] Build(deps): Bump rack-protection from 3.0.1 to 3.0.2 (#18448) Bumps [rack-protection](https://github.com/sinatra/sinatra) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/sinatra/sinatra/releases) - [Changelog](https://github.com/sinatra/sinatra/blob/master/CHANGELOG.md) - [Commits](https://github.com/sinatra/sinatra/compare/v3.0.1...v3.0.2) --- updated-dependencies: - dependency-name: rack-protection dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 ff6215699d..2c71847775 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -322,7 +322,7 @@ GEM rack (2.2.4) rack-mini-profiler (3.0.0) rack (>= 1.2.0) - rack-protection (3.0.1) + rack-protection (3.0.2) rack rack-test (2.0.2) rack (>= 1.3) From 060123143f4effead00dc7e9092bc8cc471903c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Oct 2022 23:56:37 +0200 Subject: [PATCH 028/332] Build(deps): Bump jsdom from 20.0.0 to 20.0.1 in /app/assets/javascripts (#18452) Bumps [jsdom](https://github.com/jsdom/jsdom) from 20.0.0 to 20.0.1. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/master/Changelog.md) - [Commits](https://github.com/jsdom/jsdom/compare/20.0.0...20.0.1) --- updated-dependencies: - dependency-name: jsdom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- app/assets/javascripts/discourse/package.json | 2 +- app/assets/javascripts/yarn.lock | 138 +++++++++--------- 2 files changed, 73 insertions(+), 67 deletions(-) diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 5ba3b11d91..0c527390c9 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -71,7 +71,7 @@ "eslint-plugin-qunit": "^6.2.0", "html-entities": "^2.3.3", "js-yaml": "^4.1.0", - "jsdom": "^20.0.0", + "jsdom": "^20.0.1", "loader.js": "^4.7.0", "markdown-it": "^13.0.1", "message-bus-client": "^4.2.0", diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index 8cc2cc6eb1..febf62ec39 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -1881,13 +1881,13 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-globals@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" - integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== +acorn-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" + integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== dependencies: - acorn "^7.1.1" - acorn-walk "^7.1.1" + acorn "^8.1.0" + acorn-walk "^8.0.2" acorn-import-assertions@^1.7.6: version "1.8.0" @@ -1899,25 +1899,25 @@ acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== -acorn-walk@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" - integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.0.2: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -acorn@^7.1.1, acorn@^7.4.0: +acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.7.1: - version "8.7.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" - integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== +acorn@^8.1.0, acorn@^8.7.1, acorn@^8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== after@0.8.2: version "0.8.2" @@ -3207,11 +3207,6 @@ brorand@^1.0.1, brorand@^1.1.0: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= -browser-process-hrtime@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" - integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== - browser-split@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/browser-split/-/browser-split-0.0.1.tgz#7b097574f8e3ead606fb4664e64adfdda2981a93" @@ -4100,10 +4095,10 @@ debug@~4.1.0: dependencies: ms "^2.1.1" -decimal.js@^10.3.1: - version "10.3.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" - integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== +decimal.js@^10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.1.tgz#be75eeac4a2281aace80c1a8753587c27ef053e7" + integrity sha512-F29o+vci4DodHYT9UrR5IEbfBw9pE5eSapIJdTqXK5+6hq+t8VRxwQyKlW2i+KDKFkkJQRvFyI/QXD83h8LyQw== decode-uri-component@^0.2.0: version "0.2.0" @@ -5069,10 +5064,10 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@^4.3.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.1.tgz#c34062a94c865c322f9d67b4384e4169bcede6a4" - integrity sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg== +entities@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" + integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== entities@~1.1.1: version "1.1.2" @@ -7132,18 +7127,18 @@ js-yaml@^4.0.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsdom@^20.0.0: - version "20.0.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.0.tgz#882825ac9cc5e5bbee704ba16143e1fa78361ebf" - integrity sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA== +jsdom@^20.0.1: + version "20.0.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.1.tgz#d95b4a3b6e1eec6520aa01d9d908eade8c6ba153" + integrity sha512-pksjj7Rqoa+wdpkKcLzQRHhJCEE42qQhl/xLMUKHgoSejaKOdaXEAnqs6uDNwMl/fciHTzKeR8Wm8cw7N+g98A== dependencies: abab "^2.0.6" - acorn "^8.7.1" - acorn-globals "^6.0.0" + acorn "^8.8.0" + acorn-globals "^7.0.0" cssom "^0.5.0" cssstyle "^2.3.0" data-urls "^3.0.2" - decimal.js "^10.3.1" + decimal.js "^10.4.1" domexception "^4.0.0" escodegen "^2.0.0" form-data "^4.0.0" @@ -7151,18 +7146,17 @@ jsdom@^20.0.0: http-proxy-agent "^5.0.0" https-proxy-agent "^5.0.1" is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.0" - parse5 "^7.0.0" + nwsapi "^2.2.2" + parse5 "^7.1.1" saxes "^6.0.0" symbol-tree "^3.2.4" - tough-cookie "^4.0.0" - w3c-hr-time "^1.0.2" + tough-cookie "^4.1.2" w3c-xmlserializer "^3.0.0" webidl-conversions "^7.0.0" whatwg-encoding "^2.0.0" whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" - ws "^8.8.0" + ws "^8.9.0" xml-name-validator "^4.0.0" jsesc@^1.3.0: @@ -8280,10 +8274,10 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= -nwsapi@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" - integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== +nwsapi@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" + integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== object-assign@4.1.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" @@ -8551,12 +8545,12 @@ parse5@^6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== -parse5@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a" - integrity sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g== +parse5@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.1.tgz#4649f940ccfb95d8754f37f73078ea20afe0c746" + integrity sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg== dependencies: - entities "^4.3.0" + entities "^4.4.0" parseqs@0.0.6: version "0.0.6" @@ -8951,6 +8945,11 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -10432,14 +10431,15 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -tough-cookie@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" - integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== +tough-cookie@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" + integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== dependencies: psl "^1.1.33" punycode "^2.1.1" - universalify "^0.1.2" + universalify "^0.2.0" + url-parse "^1.5.3" tr46@^3.0.0: version "3.0.0" @@ -10641,11 +10641,16 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -universalify@^0.1.0, universalify@^0.1.2: +universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" @@ -10696,6 +10701,14 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -10787,13 +10800,6 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -w3c-hr-time@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" - integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== - dependencies: - browser-process-hrtime "^1.0.0" - w3c-xmlserializer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923" @@ -11120,10 +11126,10 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^8.8.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.0.tgz#8e71c75e2f6348dbf8d78005107297056cb77769" - integrity sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ== +ws@^8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e" + integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg== ws@~7.4.2: version "7.4.4" From c5544a762401e144fcd6dad5b8725e4c8b96532b Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Mon, 3 Oct 2022 16:59:25 +0800 Subject: [PATCH 029/332] FIX: Review sidebar link showing for users that can't review (#18454) --- .../lib/sidebar/user/community-section/review-section-link.js | 1 + .../tests/acceptance/sidebar-user-community-section-test.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js index 04716cca09..b0e5e83c2a 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js @@ -26,6 +26,7 @@ export default class ReviewSectionLink extends BaseSectionLink { _refreshCanDisplay() { if (!this.currentUser.can_review) { this.canDisplay = false; + return; } if (this.inMoreDrawer) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js index e56736f273..d48d92e0d1 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js @@ -711,7 +711,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) { }); test("review link is not shown when user cannot review", async function (assert) { - updateCurrentUser({ can_review: false }); + updateCurrentUser({ can_review: false, reviewable_count: 0 }); await visit("/"); From a5fbdba9d46232922f299ba18806b368d6c1da2b Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 3 Oct 2022 11:55:50 -0400 Subject: [PATCH 030/332] UX: add max-width to digest email, format erb (#18445) --- app/views/user_notifications/digest.html.erb | 752 +++++++++---------- 1 file changed, 370 insertions(+), 382 deletions(-) diff --git a/app/views/user_notifications/digest.html.erb b/app/views/user_notifications/digest.html.erb index e7750ecbdd..b55ee1976a 100644 --- a/app/views/user_notifications/digest.html.erb +++ b/app/views/user_notifications/digest.html.erb @@ -1,404 +1,392 @@
- - - -<%- if I18n.t('user_notifications.digest.custom.html.header').present? %> - + +
- - -
- <%= raw(t 'user_notifications.digest.custom.html.header') %> -
-<%- else %> - - - - -
- - - <%- if logo_url.blank? %> - <%= SiteSetting.title %> - <%- else %> - <%= SiteSetting.title %> - <%- end %> - - -
-<%- end %> - - - - - - -
-
- - + + - - - -
+ <%- if I18n.t('user_notifications.digest.custom.html.header').present? %> + - + + + - -
  + <%= raw(t 'user_notifications.digest.custom.html.header') %> +
- -
- - +
+ <%- else %> + + + + + + +
+ + <%- if logo_url.blank? %> + <%= SiteSetting.title %> + <%- else %> + <%= SiteSetting.title %> + <%- end %> + +
+ <%- end %> + - + + - -
+ +
+ + + + + + +
 
+
+
+ + + + + + +
+
+
<%=t 'user_notifications.digest.since_last_visit' %>
+ + + + <%- @counts.each do |count| -%> + + <%- end -%> + + + <%- @counts.each do |count| -%> + + <%- end -%> + + +
+ <%= count[:value] -%> +
+ <%=t count[:label_key] -%> +
+ +
+ + + + + + +
+
+ +
+ + + + + + +
 
+
- - - - - - -
-
- -
- - - - - -
 
-
-
+ <% if @popular_posts.present? %> + + + + + + + + +
  -<% if @popular_posts.present? %> + <% @popular_posts.each do |post| %> - + + + + + + + +
+ <%= email_excerpt(post.cooked, post) %> +
- - - - - - - -
  + + + + + + +
+
+
-<% @popular_posts.each do |post| %> + + + + + + + + + - - - - - - - -
- <%= email_excerpt(post.cooked, post) %> -
+
+ + +
 
+
+ - - - - - - -
-
-
+ <% end %> - - - - - + + + <% end %> -
-

- <%= gsub_emoji_to_unicode(post.topic.title.truncate(100, separator: /\s/)) -%> -

- - <%=t 'user_notifications.digest.join_the_discussion' %> - -
-
- - + <%= digest_custom_html("above_popular_topics") %> + + <% if @other_new_for_you.present? %> + +
<%=t 'user_notifications.digest.more_new' %>
+ +
 
+ + + + + +
  + + + + + <% @other_new_for_you.each do |t| %> + + + + + + + + + + + + <% end %> + + +
+ + <%= gsub_emoji_to_unicode(t.title.truncate(100, separator: /\s/)) -%> + + <%- if SiteSetting.show_topic_featured_link_in_digest && t.featured_link %> + <%= raw topic_featured_link_domain(t.featured_link) %> + <%- end %> +

+ <%= category_badge(t.category, inline_style: true, absolute_url: true) %> +

+
+ likes +

<%= t.like_count -%>

+
+ replies +

<%= t.posts_count - 1 -%>

+
 
+ +
 
+ + <% end %> + + <%= digest_custom_html("below_popular_topics") %> + + + + <%= digest_custom_html("above_footer") %> + + + + + + + + + + <%= digest_custom_html("below_footer") %> +
- - -<% end %> - -
 
- -<% end %> - - -<%= digest_custom_html("above_popular_topics") %> - -<% if @other_new_for_you.present? %> -
<%=t 'user_notifications.digest.more_new' %>
- - - - - - - -
  - - - - -<% @other_new_for_you.each do |t| %> - - - - - - - - - - - -<% end %> - - -
- - <%= gsub_emoji_to_unicode(t.title.truncate(100, separator: /\s/)) -%> - - <%- if SiteSetting.show_topic_featured_link_in_digest && t.featured_link %> - <%= raw topic_featured_link_domain(t.featured_link) %> - <%- end %> -

- <%= category_badge(t.category, inline_style: true, absolute_url: true) %> -

-
- likes -

<%= t.like_count -%>

-
- replies -

<%= t.posts_count - 1 -%>

-
 
- -
 
- -<% end %> - -<%= digest_custom_html("below_popular_topics") %> - - - -   - - - - - -<%= digest_custom_html("above_footer") %> - - - - - - - -<%= digest_custom_html("below_footer") %> - -
From 6affbabfd3a09c09b8800a8b0f81f2a07498c2fb Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Mon, 3 Oct 2022 12:19:41 -0600 Subject: [PATCH 031/332] FIX: New general category changes preventing topic create (#18459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * FIX: New general category changes preventing topic create Follow up to: #18383 The logic in the previous commit was checking for null, but we are seeding the SiteSetting.general_category_id with an id of -1 so we need to check for a positive value as well as checking for null. See: https://meta.discourse.org/t/240661 * Add js test for presence of category… dropdown option --- .../discourse/app/models/composer.js | 3 ++- .../select-kit/category-chooser-test.js | 17 +++++++++++++++++ .../addon/components/category-chooser.js | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/app/models/composer.js b/app/assets/javascripts/discourse/app/models/composer.js index bf548f633c..466491ea4b 100644 --- a/app/assets/javascripts/discourse/app/models/composer.js +++ b/app/assets/javascripts/discourse/app/models/composer.js @@ -145,7 +145,8 @@ const Composer = RestModel.extend({ if (isEmpty(categoryId)) { // Set General as the default category const generalCategoryId = this.siteSettings.general_category_id; - categoryId = generalCategoryId ? generalCategoryId : null; + categoryId = + generalCategoryId && generalCategoryId > 0 ? generalCategoryId : null; } this._categoryId = categoryId; diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js index 5446f009a8..c99ad0e64b 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js @@ -143,6 +143,23 @@ module( assert.strictEqual(this.subject.header().label(), ""); }); + test("with allowUncategorized=null and generalCategoryId present, but not set", async function (assert) { + this.siteSettings.allow_uncategorized_topics = false; + this.siteSettings.general_category_id = -1; + + await render(hbs` + + `); + + assert.strictEqual(this.subject.header().value(), null); + assert.strictEqual(this.subject.header().label(), "category…"); + }); + test("with allowUncategorized=null none=true", async function (assert) { this.siteSettings.allow_uncategorized_topics = false; diff --git a/app/assets/javascripts/select-kit/addon/components/category-chooser.js b/app/assets/javascripts/select-kit/addon/components/category-chooser.js index 2345fb2f61..7193d6457f 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-chooser.js +++ b/app/assets/javascripts/select-kit/addon/components/category-chooser.js @@ -45,7 +45,7 @@ export default ComboBoxComponent.extend({ return Category.findUncategorized(); } else { const generalCategoryId = this.siteSettings.general_category_id; - if (!generalCategoryId) { + if (!generalCategoryId || generalCategoryId < 0) { return this.defaultItem(null, htmlSafe(I18n.t("category.choose"))); } } From bd8e31cde99af862a610490c9a908c1a4f38ee38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Oct 2022 01:33:01 +0200 Subject: [PATCH 032/332] Build(deps): Bump sinon from 14.0.0 to 14.0.1 in /app/assets/javascripts (#18463) Bumps [sinon](https://github.com/sinonjs/sinon) from 14.0.0 to 14.0.1. - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md) - [Commits](https://github.com/sinonjs/sinon/compare/v14.0.0...v14.0.1) --- updated-dependencies: - dependency-name: sinon dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- app/assets/javascripts/discourse/package.json | 2 +- app/assets/javascripts/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 0c527390c9..ea0a69d384 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -83,7 +83,7 @@ "qunit-dom": "^2.0.0", "sass": "^1.55.0", "select-kit": "^1.0.0", - "sinon": "^14.0.0", + "sinon": "^14.0.1", "tippy.js": "^6.3.7", "virtual-dom": "^2.1.1", "webpack": "^5.74.0", diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index febf62ec39..3e74b59ac7 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -9632,10 +9632,10 @@ simple-html-tokenizer@^0.5.11: resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.5.11.tgz#4c5186083c164ba22a7b477b7687ac056ad6b1d9" integrity sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og== -sinon@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.0.tgz#203731c116d3a2d58dc4e3cbe1f443ba9382a031" - integrity sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw== +sinon@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.1.tgz#9f02e13ad86b695c0c554525e3bf7f8245b31a9c" + integrity sha512-JhJ0jCiyBWVAHDS+YSjgEbDn7Wgz9iIjA1/RK+eseJN0vAAWIWiXBdrnb92ELPyjsfreCYntD1ORtLSfIrlvSQ== dependencies: "@sinonjs/commons" "^1.8.3" "@sinonjs/fake-timers" "^9.1.2" From 8a83d37ea4ef0c430a142cb6d5b778c408757794 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Oct 2022 01:34:57 +0200 Subject: [PATCH 033/332] Build(deps): Bump faraday from 2.5.2 to 2.6.0 (#18462) Bumps [faraday](https://github.com/lostisland/faraday) from 2.5.2 to 2.6.0. - [Release notes](https://github.com/lostisland/faraday/releases) - [Changelog](https://github.com/lostisland/faraday/blob/main/CHANGELOG.md) - [Commits](https://github.com/lostisland/faraday/compare/v2.5.2...v2.6.0) --- updated-dependencies: - dependency-name: faraday dependency-type: indirect update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 2c71847775..65aa42ae53 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,7 +148,7 @@ GEM faker (2.23.0) i18n (>= 1.8.11, < 2) fakeweb (1.3.0) - faraday (2.5.2) + faraday (2.6.0) faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-net_http (3.0.0) From 76a79b6adf1b16408febd54305b53785661a2266 Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Tue, 4 Oct 2022 10:48:33 +0800 Subject: [PATCH 034/332] UX: Change notifications nav icon in user page to bell (#18455) Now that we have chat, the comment icon is no longer ideal Internal Ref: /t/67780/55 --- app/assets/javascripts/discourse/app/components/user-nav.hbs | 2 +- app/assets/javascripts/discourse/app/templates/user.hbs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/user-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav.hbs index 237b2a19ad..6988e134fa 100644 --- a/app/assets/javascripts/discourse/app/components/user-nav.hbs +++ b/app/assets/javascripts/discourse/app/components/user-nav.hbs @@ -52,7 +52,7 @@ {{#if @showNotificationsTab}}
  • - {{d-icon "comment" class="glyph"}}{{i18n "user.notifications"}} + {{d-icon "bell" class="glyph"}}{{i18n "user.notifications"}}
  • {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/user.hbs b/app/assets/javascripts/discourse/app/templates/user.hbs index c3b1e4e419..fa522ce8b2 100644 --- a/app/assets/javascripts/discourse/app/templates/user.hbs +++ b/app/assets/javascripts/discourse/app/templates/user.hbs @@ -267,7 +267,7 @@ {{#if this.showNotificationsTab}}
  • - {{d-icon "comment" class="glyph"}}{{i18n 'user.notifications'}} + {{d-icon "bell" class="glyph"}}{{i18n 'user.notifications'}}
  • {{/if}} From de071fc1e8135750c9227424fb41b41d649f35dc Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Tue, 4 Oct 2022 12:05:09 +0800 Subject: [PATCH 035/332] DEV: Convert messages user page nav to experimental redesign (#18456) No tests are written for now as we're still in a highly iterative stage --- .../user-nav/messages-groups-dropdown.js | 53 ++++++ .../app/components/user-nav/messages-nav.hbs | 105 +++++++++++ .../app/components/user-nav/messages-nav.js | 24 +++ .../app/controllers/user-private-messages.js | 5 +- .../discourse/app/templates/user/messages.hbs | 171 ++++++++++-------- app/assets/stylesheets/desktop/new-user.scss | 17 ++ app/assets/stylesheets/mobile/new-user.scss | 13 ++ config/locales/client.en.yml | 1 + 8 files changed, 313 insertions(+), 76 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/user-nav/messages-groups-dropdown.js create mode 100644 app/assets/javascripts/discourse/app/components/user-nav/messages-nav.hbs create mode 100644 app/assets/javascripts/discourse/app/components/user-nav/messages-nav.js diff --git a/app/assets/javascripts/discourse/app/components/user-nav/messages-groups-dropdown.js b/app/assets/javascripts/discourse/app/components/user-nav/messages-groups-dropdown.js new file mode 100644 index 0000000000..3d5502d957 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-nav/messages-groups-dropdown.js @@ -0,0 +1,53 @@ +import { gte, reads } from "@ember/object/computed"; +import ComboBoxComponent from "select-kit/components/combo-box"; +import DiscourseURL from "discourse/lib/url"; +import I18n from "I18n"; +import { computed } from "@ember/object"; + +export default ComboBoxComponent.extend({ + pluginApiIdentifiers: ["messages-dropdown"], + classNames: ["message-dropdown"], + content: reads("groupsWithMessages"), + valueProperty: "name", + hasManyGroups: gte("content.length", 10), + + selectKitOptions: { + caretDownIcon: "caret-right", + caretUpIcon: "caret-down", + filterable: "hasManyGroups", + }, + + groupsWithMessages: computed(function () { + const groups = [ + { + name: I18n.t("user.messages.inbox"), + }, + ]; + + this.user.groupsWithMessages.forEach((group) => { + groups.push({ name: group.name, icon: "inbox" }); + }); + + if (this.pmTaggingEnabled) { + groups.push({ name: I18n.t("user.messages.tags") }); + } + + return groups; + }), + + actions: { + onChange(item) { + let url; + + if (this.user.groups.some((g) => g.name === item)) { + url = `/u/${this.user.username}/messages/group/${item}`; + } else if (item === I18n.t("user.messages.tags")) { + url = `/u/${this.user.username}/messages/tags`; + } else { + url = `/u/${this.user.username}/messages`; + } + + DiscourseURL.routeToUrl(url); + }, + }, +}); diff --git a/app/assets/javascripts/discourse/app/components/user-nav/messages-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav/messages-nav.hbs new file mode 100644 index 0000000000..a35091d7a7 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-nav/messages-nav.hbs @@ -0,0 +1,105 @@ +
    + {{#if (gt @user.groupsWithMessages.length 0)}} +
      +
    1. + +
    2. +
    + {{/if}} + + + + {{#if this.site.desktopView}} + + {{/if}} +
    diff --git a/app/assets/javascripts/discourse/app/components/user-nav/messages-nav.js b/app/assets/javascripts/discourse/app/components/user-nav/messages-nav.js new file mode 100644 index 0000000000..b89709d85c --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-nav/messages-nav.js @@ -0,0 +1,24 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; + +export default class UserNavMessagesNav extends Component { + @service site; + + get messagesDropdownvalue() { + switch (this.args.currentRouteName) { + case "userPrivateMessages.tags": + case "userPrivateMessages.tagsShow": + return "tags"; + default: + if (this.args.groupFilter) { + return this.args.groupFilter; + } else { + return "inbox"; + } + } + } + + get displayTags() { + return this.args.pmTaggingEnabled && this.messagesDropdownvalue === "tags"; + } +} diff --git a/app/assets/javascripts/discourse/app/controllers/user-private-messages.js b/app/assets/javascripts/discourse/app/controllers/user-private-messages.js index e0ec47bbe5..4b9e10fa47 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-private-messages.js +++ b/app/assets/javascripts/discourse/app/controllers/user-private-messages.js @@ -1,6 +1,7 @@ import Controller, { inject as controller } from "@ember/controller"; import { action } from "@ember/object"; -import { alias, and, equal } from "@ember/object/computed"; +import { inject as service } from "@ember/service"; +import { alias, and, equal, readOnly } from "@ember/object/computed"; import discourseComputed from "discourse-common/utils/decorators"; import { VIEW_NAME_WARNINGS } from "discourse/routes/user-private-messages-warnings"; import I18n from "I18n"; @@ -9,6 +10,7 @@ export const PERSONAL_INBOX = "__personal_inbox__"; export default Controller.extend({ user: controller(), + router: service(), pmView: false, viewingSelf: alias("user.viewingSelf"), @@ -17,6 +19,7 @@ export default Controller.extend({ group: null, groupFilter: alias("group.name"), currentPath: alias("router._router.currentPath"), + currentRouteName: readOnly("router.currentRouteName"), pmTaggingEnabled: alias("site.can_tag_pms"), tagId: null, diff --git a/app/assets/javascripts/discourse/app/templates/user/messages.hbs b/app/assets/javascripts/discourse/app/templates/user/messages.hbs index bf566c7ba5..5ca63d8f2c 100644 --- a/app/assets/javascripts/discourse/app/templates/user/messages.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/messages.hbs @@ -1,103 +1,124 @@ - - -
  • - - {{i18n "user.messages.inbox"}} - -
  • +{{#if this.currentUser.redesigned_user_page_nav_enabled}} + - {{#if this.isPersonal}} -
  • - - {{i18n "user.messages.sent"}} + +{{else}} + + +
  • + + {{i18n "user.messages.inbox"}}
  • - {{#if this.viewingSelf}} + {{#if this.isPersonal}}
  • - - {{this.newLinkText}} + + {{i18n "user.messages.sent"}}
  • + {{#if this.viewingSelf}} +
  • + + {{this.newLinkText}} + +
  • + +
  • + + {{this.unreadLinkText}} + +
  • + {{/if}} +
  • - - {{this.unreadLinkText}} + + {{i18n "user.messages.archive"}}
  • {{/if}} -
  • - - {{i18n "user.messages.archive"}} - -
  • - {{/if}} + {{#each this.model.groups as |group|}} + {{#if group.has_messages}} +
  • + + {{d-icon "users"}} + {{capitalize-string group.name}} + +
  • - {{#each this.model.groups as |group|}} - {{#if group.has_messages}} -
  • - - {{d-icon "users"}} - {{capitalize-string group.name}} - -
  • + {{#if (and this.isGroup (eq this.groupFilter group.name))}} + {{#if this.viewingSelf}} +
  • + + {{this.newLinkText}} + +
  • - {{#if (and this.isGroup (eq this.groupFilter group.name))}} - {{#if this.viewingSelf}} -
  • - - {{this.newLinkText}} - -
  • +
  • + + {{this.unreadLinkText}} + +
  • + {{/if}}
  • - - {{this.unreadLinkText}} + + {{i18n "user.messages.archive"}}
  • {{/if}} - -
  • - - {{i18n "user.messages.archive"}} - -
  • {{/if}} + {{/each}} + + {{#if this.pmTaggingEnabled}} +
  • + + {{i18n "user.messages.tags"}} + + + {{#if this.tagId}} +
  • + + {{this.tagId}} + +
  • + {{/if}} + {{/if}} - {{/each}} - {{#if this.pmTaggingEnabled}} -
  • - - {{i18n "user.messages.tags"}} - + + + - {{#if this.tagId}} -
  • - - {{this.tagId}} - -
  • - {{/if}} - - {{/if}} + {{#unless this.site.mobileView}} +
    + {{#if this.group}} + + {{/if}} - - - - -{{#unless this.site.mobileView}} -
    - {{#if this.group}} - - {{/if}} - - {{#if this.showNewPM}} - - {{/if}} -
    -{{/unless}} + {{#if this.showNewPM}} + + {{/if}} +
    + {{/unless}} +{{/if}}
    diff --git a/app/assets/stylesheets/desktop/new-user.scss b/app/assets/stylesheets/desktop/new-user.scss index f360fd6551..dd378295a6 100644 --- a/app/assets/stylesheets/desktop/new-user.scss +++ b/app/assets/stylesheets/desktop/new-user.scss @@ -14,6 +14,23 @@ grid-row-end: 2; } + .user-navigation-container { + grid-column-start: 1; + grid-column-end: 3; + grid-row-start: 1; + grid-row-end: 2; + + display: flex; + flex-direction: row; + } + + .user-navigation-container ~ .user-content { + grid-column-start: 1; + grid-column-end: 3; + grid-row-start: 2; + grid-row-end: 3; + } + .user-content { grid-column-start: 1; grid-column-end: 3; diff --git a/app/assets/stylesheets/mobile/new-user.scss b/app/assets/stylesheets/mobile/new-user.scss index 24b6acc3fc..8fe72e0053 100644 --- a/app/assets/stylesheets/mobile/new-user.scss +++ b/app/assets/stylesheets/mobile/new-user.scss @@ -49,4 +49,17 @@ @include ellipsis; } } + + #navigation-bar { + margin-top: 1em; + display: flex; + flex-wrap: nowrap; + width: 100%; + overflow-x: scroll; + margin-bottom: 0; + padding-bottom: 0.5em; + a { + white-space: nowrap; + } + } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index aabda302ec..3a485e8d00 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1275,6 +1275,7 @@ en: move_to_archive: "Archive" failed_to_move: "Failed to move selected messages (perhaps your network is down)" tags: "Tags" + all_tags: "All Tags" warnings: "Official Warnings" read_more_in_group: "Want to read more? Browse other messages in %{groupLink}." read_more: "Want to read more? Browse other messages in personal messages." From 044dc853589197d2bc323c816045fd9c66cb771c Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Tue, 4 Oct 2022 13:37:25 +0800 Subject: [PATCH 036/332] DEV: Remove broken line of code (#18465) The router service isn't even injected so the code is not used and broken. --- .../discourse/app/controllers/user-private-messages.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/discourse/app/controllers/user-private-messages.js b/app/assets/javascripts/discourse/app/controllers/user-private-messages.js index 4b9e10fa47..6e598d6fa0 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-private-messages.js +++ b/app/assets/javascripts/discourse/app/controllers/user-private-messages.js @@ -18,7 +18,6 @@ export default Controller.extend({ isPersonal: equal("pmView", "user"), group: null, groupFilter: alias("group.name"), - currentPath: alias("router._router.currentPath"), currentRouteName: readOnly("router.currentRouteName"), pmTaggingEnabled: alias("site.can_tag_pms"), tagId: null, From ba2c5c7948e17db5a64f6491c60329f74b87ad16 Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Tue, 4 Oct 2022 14:23:20 +0800 Subject: [PATCH 037/332] UX: Hide sidebar on 2FA route (#18464) Internal Ref: /t/75929 --- .../discourse/app/controllers/application.js | 30 +++++++++---------- .../app/routes/second-factor-auth.js | 13 ++++++++ .../acceptance/second-factor-auth-test.js | 9 ++++++ .../javascripts/wizard/addon/routes/wizard.js | 10 ++++++- 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/discourse/app/controllers/application.js b/app/assets/javascripts/discourse/app/controllers/application.js index a7aede86aa..1f4c3c7cd5 100644 --- a/app/assets/javascripts/discourse/app/controllers/application.js +++ b/app/assets/javascripts/discourse/app/controllers/application.js @@ -7,13 +7,15 @@ import { action } from "@ember/object"; const HIDE_SIDEBAR_KEY = "sidebar-hidden"; export default Controller.extend({ - queryParams: ["enable_sidebar"], + queryParams: [{ sidebarQueryParamOverride: "enable_sidebar" }], showTop: true, showFooter: false, router: service(), showSidebar: false, - enable_sidebar: null, + sidebarQueryParamOverride: null, + sidebarDisabledRouteOverride: false, + showSiteHeader: true, init() { this._super(...arguments); @@ -62,20 +64,25 @@ export default Controller.extend({ }, @discourseComputed( - "enable_sidebar", + "sidebarQueryParamOverride", "siteSettings.enable_sidebar", - "router.currentRouteName", - "canDisplaySidebar" + "canDisplaySidebar", + "sidebarDisabledRouteOverride" ) sidebarEnabled( sidebarQueryParamOverride, enableSidebar, - currentRouteName, - canDisplaySidebar + canDisplaySidebar, + sidebarDisabledRouteOverride ) { if (!canDisplaySidebar) { return false; } + + if (sidebarDisabledRouteOverride) { + return false; + } + if (sidebarQueryParamOverride === "1") { return true; } @@ -84,10 +91,6 @@ export default Controller.extend({ return false; } - if (currentRouteName.startsWith("wizard")) { - return false; - } - // Always return dropdown on mobile if (this.site.mobileView) { return false; @@ -96,11 +99,6 @@ export default Controller.extend({ return enableSidebar; }, - @discourseComputed("router.currentRouteName") - showSiteHeader(currentRouteName) { - return !currentRouteName.startsWith("wizard"); - }, - @action toggleSidebar() { // enables CSS transitions, but not on did-insert diff --git a/app/assets/javascripts/discourse/app/routes/second-factor-auth.js b/app/assets/javascripts/discourse/app/routes/second-factor-auth.js index 61d71fa5ab..50bfee3e1a 100644 --- a/app/assets/javascripts/discourse/app/routes/second-factor-auth.js +++ b/app/assets/javascripts/discourse/app/routes/second-factor-auth.js @@ -26,9 +26,22 @@ export default DiscourseRoute.extend({ } }, + activate() { + this.controllerFor("application").setProperties({ + sidebarDisabledRouteOverride: true, + }); + }, + + deactivate() { + this.controllerFor("application").setProperties({ + sidebarDisabledRouteOverride: false, + }); + }, + setupController(controller, model) { this._super(...arguments); controller.resetState(); + if (model.error) { controller.displayError(model.error); controller.set("loadError", true); diff --git a/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js b/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js index f2ca750155..32b57a7e6d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js @@ -283,4 +283,13 @@ acceptance("Second Factor Auth Page", function (needs) { ); assert.equal(callbackCount, 1, "callback request has been performed"); }); + + test("sidebar is disabled on 2FA route", async function (assert) { + this.siteSettings.enable_experimental_sidebar_hamburger = true; + this.siteSettings.enable_sidebar = true; + + await visit("/session/2fa?nonce=ok110111"); + + assert.notOk(exists(".sidebar-container"), "does not display the sidebar"); + }); }); diff --git a/app/assets/javascripts/wizard/addon/routes/wizard.js b/app/assets/javascripts/wizard/addon/routes/wizard.js index 73aa06c142..0048317818 100644 --- a/app/assets/javascripts/wizard/addon/routes/wizard.js +++ b/app/assets/javascripts/wizard/addon/routes/wizard.js @@ -8,14 +8,22 @@ export default Route.extend({ activate() { document.body.classList.add("wizard"); + this.controllerFor("application").setProperties({ showTop: false, showFooter: false, + sidebarDisabledRouteOverride: true, + showSiteHeader: false, }); }, deactivate() { document.body.classList.remove("wizard"); - this.controllerFor("application").set("showTop", true); + + this.controllerFor("application").setProperties({ + showTop: true, + sidebarDisabledRouteOverride: false, + showSiteHeader: true, + }); }, }); From fe12817cb7a8f49a39a9db0d5db43ec457bd526e Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Tue, 4 Oct 2022 10:35:27 +0400 Subject: [PATCH 038/332] FIX: do not show user status on posts twice (#18458) --- .../discourse/app/widgets/poster-name.js | 19 +++++++++---------- .../acceptance/topic-user-status-test.js | 17 +++++++++++++++++ .../stylesheets/common/base/topic-post.scss | 4 ---- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/discourse/app/widgets/poster-name.js b/app/assets/javascripts/discourse/app/widgets/poster-name.js index 77a79fed30..c5f28601ea 100644 --- a/app/assets/javascripts/discourse/app/widgets/poster-name.js +++ b/app/assets/javascripts/discourse/app/widgets/poster-name.js @@ -122,7 +122,9 @@ export default createWidget("poster-name", { } } - const afterNameContents = this.afterNameContents(attrs); + const afterNameContents = + applyDecorators(this, "after-name", attrs, this.state) || []; + nameContents = nameContents.concat(afterNameContents); const contents = [ @@ -160,19 +162,16 @@ export default createWidget("poster-name", { ); } + if (this.siteSettings.enable_user_status) { + this.addUserStatus(contents, attrs); + } + return contents; }, - afterNameContents(attrs) { - const contents = []; - if ( - this.siteSettings.enable_user_status && - attrs.user && - attrs.user.status - ) { + addUserStatus(contents, attrs) { + if (attrs.user && attrs.user.status) { contents.push(this.attach("post-user-status", attrs.user.status)); } - contents.push(...applyDecorators(this, "after-name", attrs, this.state)); - return contents; }, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-user-status-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-user-status-test.js index c8960b64fb..42310bc3a9 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-user-status-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-user-status-test.js @@ -19,6 +19,10 @@ acceptance("Topic - User Status", function (needs) { const response = cloneJSON(TopicFixtures["/t/299/1.json"]); response.post_stream.posts.forEach((post) => { post.user_status = status; + + // we need the poster's name to be different from username + // so when display_name_on_posts = true, both name and username will be shown: + post.name = "Evil T"; }); return helper.response(200, response); @@ -35,6 +39,19 @@ acceptance("Topic - User Status", function (needs) { "all posts has user status" ); }); + + test("shows user status next to avatar on posts when displaying names on posts is enabled", async function (assert) { + this.siteSettings.enable_user_status = true; + this.siteSettings.display_name_on_posts = true; + + await visit("/t/-/299/1"); + + assert.equal( + queryAll(".topic-post .user-status-message").length, + 3, + "all posts has user status" + ); + }); }); acceptance("Topic - User Status - live updates", function (needs) { diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index bd2632d0f2..6ccb5eae0a 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -78,10 +78,6 @@ $quote-share-maxwidth: 150px; .user-title a { color: var(--primary-med-or-secondary-med); } - - .user-status-message { - margin-left: 0.3em; - } } // global styles for the cooked HTML content in posts (and preview) From c654bdbfa949672459070b55ac7bc533bb06c64a Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Tue, 4 Oct 2022 15:18:54 +0800 Subject: [PATCH 039/332] DEV: Experimental changes to user page notifications nav (#18466) No tests as the changes are experimental and unconfirmed Internal Ref: /t/67780/58 --- .../app/templates/user/notifications.hbs | 112 +++++++++++++----- 1 file changed, 80 insertions(+), 32 deletions(-) diff --git a/app/assets/javascripts/discourse/app/templates/user/notifications.hbs b/app/assets/javascripts/discourse/app/templates/user/notifications.hbs index 9637395e67..462340cf05 100644 --- a/app/assets/javascripts/discourse/app/templates/user/notifications.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/notifications.hbs @@ -1,41 +1,89 @@ - - -
  • - - {{i18n "user.filters.all"}} - -
  • -
  • - - {{i18n "user_action_groups.6"}} - -
  • -
  • - - {{i18n "user_action_groups.2"}} - -
  • - {{#if this.siteSettings.enable_mentions}} +{{#if this.currentUser.redesigned_user_page_nav_enabled}} + + +
    + + + {{#if this.model}} + {{/if}} -
  • - - {{i18n "user_action_groups.11"}} - -
  • - - +
    +{{else}} + + +
  • + + {{i18n "user.filters.all"}} + +
  • +
  • + + {{i18n "user_action_groups.6"}} + +
  • +
  • + + {{i18n "user_action_groups.2"}} + +
  • + {{#if this.siteSettings.enable_mentions}} +
  • + + {{i18n "user_action_groups.7"}} + +
  • + {{/if}} +
  • + + {{i18n "user_action_groups.11"}} + +
  • + +
    -
    +
    -{{#if this.model}} -
    - -
    + {{#if this.model}} +
    + +
    + {{/if}} {{/if}}
    From ba27ee16376c961c93a4e3854b038a42f9577613 Mon Sep 17 00:00:00 2001 From: Dan Gebhardt Date: Tue, 4 Oct 2022 04:42:46 -0400 Subject: [PATCH 040/332] DEV: Remove usage of `{{action}}` modifiers (#18333) This PR enables the [`no-action-modifiers`](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-action-modifiers.md) template lint rule and removes all usages of the `{{action}}` modifier in core. In general, instances of `{{action "x"}}` have been replaced with `{{on "click" (action "x")}}`. In many cases, such as for `a` elements, we also need to prevent default event handling to avoid unwanted side effects. While the `{{action}}` modifier internally calls `event.preventDefault()`, we need to handle these cases more explicitly. For this purpose, this PR also adds the [ember-event-helpers](https://github.com/buschtoens/ember-event-helpers) dependency so we can use the `prevent-default` handler. For instance: ``` Do X ``` Note that `action` has not in general been refactored away as a helper yet. In general, all event handlers should be methods on the corresponding component and referenced directly (e.g. `{{on "click" this.doSomething}}`). However, the `action` helper is used extensively throughout the codebase and often references methods in the `actions` hash on controllers or routes. Thus this refactor will also be extensive and probably deserves a separate PR. Note: This work was done to complement #17767 by minimizing the potential impact of the `action` modifier override, which uses private API and arguably should be replaced with an AST transform. Commits: * Enable `no-action-modifiers` template lint rule * Replace {{action "x"}} with {{on "click" (action "x")}} * Remove unnecessary action helper usage * Remove ctl+click tests for user-menu These tests now break in Chrome when used with addEventListener. As per the comment, they can probably be safely removed. * Prevent default event handlers to avoid unwanted side effects Uses `event.preventDefault()` in event handlers to prevent default event handling. This had been done automatically by the `action` modifier, but is not always desirable or necessary. * Restore UserCardContents#showUser action to avoid regression By keeping the `showUser` action, we can avoid a breaking change for plugins that rely upon it, while not interfering with the `showUser` argument that's been passed. * Revert EditCategoryTab#selectTab -> EditCategoryTab#select Avoid potential breaking change in themes / plugins * Restore GroupCardContents#showGroup action to avoid regression By keeping the `showGroup` action, we can avoid a breaking change for plugins that rely upon it, while not interfering with the `showGroup` argument that's been passed. * Restore SecondFactorAddTotp#showSecondFactorKey action to avoid regression By keeping the `showSecondFactorKey` action, we can avoid a breaking change for plugins that rely upon it, while not interfering with the `showSecondFactorKey` property that's maintained on the controller. * Refactor away from `actions` hash in ChooseMessage component * Modernize EmojiPicker#onCategorySelection usage * Modernize SearchResultEntry#logClick usage * Modernize Discovery::Categories#showInserted usage * Modernize Preferences::Account#resendConfirmationEmail usage * Modernize MultiSelect::SelectedCategory#onSelectedNameClick usage * Favor fn over action in SelectedChoice component * Modernize WizardStep event handlers * Favor fn over action usage in buttons * Restore Login#forgotPassword action to avoid possible regression --- .template-lintrc.js | 1 + .../addon/components/admin-editable-field.js | 14 +- .../addon/components/admin-theme-editor.js | 33 +++-- .../admin/addon/components/ip-lookup.js | 12 +- .../addon/components/themes-list-item.js | 9 +- .../addon/controllers/admin-badges/show.js | 6 + .../addon/controllers/admin-email-bounced.js | 7 + .../controllers/admin-email-preview-digest.js | 12 +- .../addon/controllers/admin-email-rejected.js | 7 + .../admin-logs-screened-ip-addresses.js | 17 ++- .../admin-logs-staff-action-logs.js | 125 ++++++++++-------- .../admin-web-hooks-show-events.js | 33 ++--- .../modals/admin-uploaded-image-list.js | 11 +- .../addon/templates/admin-badges/show.hbs | 4 +- .../components/admin-editable-field.hbs | 4 +- .../components/admin-theme-editor.hbs | 6 +- .../addon/templates/components/ip-lookup.hbs | 2 +- .../templates/components/themes-list-item.hbs | 2 +- .../admin/addon/templates/email-bounced.hbs | 4 +- .../addon/templates/email-preview-digest.hbs | 4 +- .../admin/addon/templates/email-rejected.hbs | 4 +- .../templates/logs/screened-ip-addresses.hbs | 2 +- .../templates/logs/staff-action-logs.hbs | 20 +-- .../modal/admin-uploaded-image-list.hbs | 2 +- .../addon/templates/web-hooks-show-events.hbs | 2 +- .../app/components/categories-only.js | 3 +- .../app/components/category-permission-row.js | 11 +- .../app/components/choose-message.js | 15 +-- .../app/components/composer-messages.js | 12 +- .../app/components/edit-category-tab.js | 16 +-- .../discourse/app/components/emoji-picker.js | 3 +- .../app/components/group-card-contents.js | 23 +++- .../components/group-imap-email-settings.js | 3 +- .../discourse/app/components/group-member.js | 10 +- .../components/group-smtp-email-settings.js | 3 +- .../discourse/app/components/mobile-nav.js | 41 +++--- .../app/components/navigation-bar.js | 43 +++--- .../app/components/reviewable-item.js | 47 ++++--- .../app/components/reviewable-post-edits.js | 25 ++-- .../app/components/reviewable-queued-post.js | 9 +- .../app/components/search-result-entry.js | 1 + .../app/components/second-factor-form.js | 21 +-- .../app/components/security-key-form.js | 13 +- .../discourse/app/components/signup-cta.js | 12 +- .../discourse/app/components/tag-info.js | 71 +++++----- .../app/components/topic-list-item.js | 2 + .../app/components/user-card-contents.js | 16 ++- .../discourse/app/controllers/auth-token.js | 11 +- .../app/controllers/avatar-selector.js | 17 ++- .../discourse/app/controllers/composer.js | 43 +++--- .../app/controllers/discovery/categories.js | 19 +-- .../app/controllers/discovery/topics.js | 21 +-- .../app/controllers/full-page-search.js | 31 +++-- .../discourse/app/controllers/history.js | 29 ++-- .../discourse/app/controllers/login.js | 111 +++++++++------- .../app/controllers/password-reset.js | 13 +- .../app/controllers/preferences/account.js | 28 ++-- .../controllers/preferences/second-factor.js | 40 +++--- .../app/controllers/preferences/security.js | 98 +++++++------- .../app/controllers/second-factor-add-totp.js | 9 +- .../app/controllers/second-factor-auth.js | 3 +- .../discourse/app/controllers/tag-show.js | 3 +- .../discourse/app/controllers/tags-index.js | 37 +++--- .../discourse/app/controllers/topic.js | 87 ++++++------ .../controllers/user-private-messages-tags.js | 34 ++--- .../app/controllers/user-topics-list.js | 4 +- .../discourse/app/controllers/user.js | 16 ++- .../templates/components/categories-only.hbs | 2 +- .../components/category-permission-row.hbs | 2 +- .../templates/components/choose-message.hbs | 2 +- .../components/edit-category-tab.hbs | 2 +- .../components/emoji-group-buttons.hbs | 18 +-- .../app/templates/components/emoji-picker.hbs | 6 +- .../templates/components/flag-action-type.hbs | 4 +- .../components/group-card-contents.hbs | 8 +- .../components/group-imap-email-settings.hbs | 2 +- .../group-manage-email-settings.hbs | 4 +- .../app/templates/components/group-member.hbs | 2 +- .../components/group-smtp-email-settings.hbs | 2 +- .../templates/components/login-buttons.hbs | 2 +- .../templates/components/reviewable-item.hbs | 2 +- .../components/reviewable-post-edits.hbs | 2 +- .../components/reviewable-queued-post.hbs | 2 +- .../components/search-result-entry.hbs | 2 +- .../components/second-factor-form.hbs | 2 +- .../components/security-key-form.hbs | 2 +- .../templates/components/selected-posts.hbs | 6 +- .../app/templates/components/signup-cta.hbs | 2 +- .../app/templates/components/tag-info.hbs | 6 +- .../components/user-card-contents.hbs | 6 +- .../discourse/app/templates/composer.hbs | 10 +- .../templates/composer/dominating-topic.hbs | 2 +- .../app/templates/composer/education.hbs | 2 +- .../app/templates/composer/get-a-room.hbs | 2 +- .../templates/composer/group-mentioned.hbs | 2 +- .../app/templates/composer/similar-topics.hbs | 2 +- .../app/templates/discovery/categories.hbs | 2 +- .../app/templates/discovery/topics.hbs | 2 +- .../app/templates/full-page-search.hbs | 2 +- .../mobile/components/categories-only.hbs | 2 +- .../mobile/components/mobile-nav.hbs | 2 +- .../mobile/components/navigation-bar.hbs | 2 +- .../app/templates/mobile/discovery/topics.hbs | 2 +- .../app/templates/mobile/modal/login.hbs | 4 +- .../app/templates/modal/auth-token.hbs | 2 +- .../app/templates/modal/avatar-selector.hbs | 2 +- .../discourse/app/templates/modal/history.hbs | 6 +- .../app/templates/modal/insert-hyperlink.hbs | 2 +- .../discourse/app/templates/modal/login.hbs | 4 +- .../modal/second-factor-add-totp.hbs | 2 +- .../app/templates/password-reset.hbs | 2 +- .../templates/preferences-second-factor.hbs | 2 +- .../app/templates/preferences/account.hbs | 2 +- .../app/templates/preferences/security.hbs | 6 +- .../app/templates/second-factor-auth.hbs | 2 +- .../discourse/app/templates/tag/show.hbs | 2 +- .../discourse/app/templates/tags/index.hbs | 4 +- .../discourse/app/templates/topic.hbs | 6 +- .../templates/user-private-messages-tags.hbs | 4 +- .../app/templates/user-topics-list.hbs | 2 +- .../discourse/app/templates/user.hbs | 2 +- .../plugin-outlet-connector-class-test.js | 4 +- .../tests/acceptance/user-menu-test.js | 19 --- .../fixtures/concerns/notification-types.js | 1 + .../multi-select/selected-category.hbs | 2 +- .../templates/components/selected-choice.hbs | 2 +- .../addon/components/styling-preview.js | 6 +- .../wizard/addon/components/wizard-step.js | 14 +- .../templates/components/styling-preview.hbs | 4 +- .../templates/components/wizard-step.hbs | 10 +- lib/tasks/javascript.rake | 2 +- .../discourse-local-dates-create-form.js | 8 +- .../discourse-local-dates-create-form.hbs | 2 +- .../controllers/poll-ui-builder.js | 6 + .../templates/modal/poll-ui-builder.hbs | 6 +- 135 files changed, 930 insertions(+), 731 deletions(-) diff --git a/.template-lintrc.js b/.template-lintrc.js index e3fc07b48d..b5095385b2 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -3,6 +3,7 @@ module.exports = { extends: "discourse:recommended", rules: { + "no-action-modifiers": true, "no-capital-arguments": false, // TODO: we extensively use `args` argument name "no-curly-component-invocation": { allow: [ diff --git a/app/assets/javascripts/admin/addon/components/admin-editable-field.js b/app/assets/javascripts/admin/addon/components/admin-editable-field.js index 993a3ed675..892a3208e8 100644 --- a/app/assets/javascripts/admin/addon/components/admin-editable-field.js +++ b/app/assets/javascripts/admin/addon/components/admin-editable-field.js @@ -1,4 +1,6 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; + export default Component.extend({ tagName: "", @@ -10,12 +12,14 @@ export default Component.extend({ this.set("editing", false); }, - actions: { - edit() { - this.set("buffer", this.value); - this.toggleProperty("editing"); - }, + @action + edit(event) { + event?.preventDefault(); + this.set("buffer", this.value); + this.toggleProperty("editing"); + }, + actions: { save() { // Action has to toggle 'editing' property. this.action(this.buffer); diff --git a/app/assets/javascripts/admin/addon/components/admin-theme-editor.js b/app/assets/javascripts/admin/addon/components/admin-theme-editor.js index 014055c149..de8f063579 100644 --- a/app/assets/javascripts/admin/addon/components/admin-theme-editor.js +++ b/app/assets/javascripts/admin/addon/components/admin-theme-editor.js @@ -3,6 +3,7 @@ import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { fmt } from "discourse/lib/computed"; import { isDocumentRTL } from "discourse/lib/text-direction"; +import { action } from "@ember/object"; import { next } from "@ember/runloop"; export default Component.extend({ @@ -91,15 +92,26 @@ export default Component.extend({ return this.theme.getError(target, fieldName); }, + @action + toggleShowAdvanced(event) { + event?.preventDefault(); + this.toggleProperty("showAdvanced"); + }, + + @action + toggleAddField(event) { + event?.preventDefault(); + this.toggleProperty("addingField"); + }, + + @action + toggleMaximize(event) { + event?.preventDefault(); + this.toggleProperty("maximized"); + next(() => this.appEvents.trigger("ace:resize")); + }, + actions: { - toggleShowAdvanced() { - this.toggleProperty("showAdvanced"); - }, - - toggleAddField() { - this.toggleProperty("addingField"); - }, - cancelAddField() { this.set("addingField", false); }, @@ -114,11 +126,6 @@ export default Component.extend({ this.fieldAdded(this.currentTargetName, name); }, - toggleMaximize() { - this.toggleProperty("maximized"); - next(() => this.appEvents.trigger("ace:resize")); - }, - onlyOverriddenChanged(value) { this.onlyOverriddenChanged(value); }, diff --git a/app/assets/javascripts/admin/addon/components/ip-lookup.js b/app/assets/javascripts/admin/addon/components/ip-lookup.js index 222e04b9ab..02a5240564 100644 --- a/app/assets/javascripts/admin/addon/components/ip-lookup.js +++ b/app/assets/javascripts/admin/addon/components/ip-lookup.js @@ -1,6 +1,6 @@ import AdminUser from "admin/models/admin-user"; import Component from "@ember/component"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import { ajax } from "discourse/lib/ajax"; import copyText from "discourse/lib/copy-text"; @@ -21,6 +21,12 @@ export default Component.extend({ return Math.max(visible, total); }, + @action + hide(event) { + event?.preventDefault(); + this.set("show", false); + }, + actions: { lookup() { this.set("show", true); @@ -55,10 +61,6 @@ export default Component.extend({ } }, - hide() { - this.set("show", false); - }, - copy() { let text = `IP: ${this.ip}\n`; const location = this.location; diff --git a/app/assets/javascripts/admin/addon/components/themes-list-item.js b/app/assets/javascripts/admin/addon/components/themes-list-item.js index e2237e249b..29158c4496 100644 --- a/app/assets/javascripts/admin/addon/components/themes-list-item.js +++ b/app/assets/javascripts/admin/addon/components/themes-list-item.js @@ -3,6 +3,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import Component from "@ember/component"; import { escape } from "pretty-text/sanitizer"; import { iconHTML } from "discourse-common/lib/icon-library"; +import { action } from "@ember/object"; const MAX_COMPONENTS = 4; @@ -59,9 +60,9 @@ export default Component.extend({ return childrenCount - MAX_COMPONENTS; }, - actions: { - toggleChildrenExpanded() { - this.toggleProperty("childrenExpanded"); - }, + @action + toggleChildrenExpanded(event) { + event?.preventDefault(); + this.toggleProperty("childrenExpanded"); }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-badges/show.js b/app/assets/javascripts/admin/addon/controllers/admin-badges/show.js index 2b38081826..5b098387ef 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-badges/show.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-badges/show.js @@ -133,6 +133,12 @@ export default class AdminBadgesShowController extends Controller.extend( this.buffered.set("image_url", null); } + @action + showPreview(badge, explain, event) { + event?.preventDefault(); + this.send("preview", badge, explain); + } + @action save() { if (!this.saving) { diff --git a/app/assets/javascripts/admin/addon/controllers/admin-email-bounced.js b/app/assets/javascripts/admin/addon/controllers/admin-email-bounced.js index 1500549564..c7a8ec98a6 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-email-bounced.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-email-bounced.js @@ -2,8 +2,15 @@ import AdminEmailLogsController from "admin/controllers/admin-email-logs"; import { INPUT_DELAY } from "discourse-common/config/environment"; import discourseDebounce from "discourse-common/lib/debounce"; import { observes } from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; export default AdminEmailLogsController.extend({ + @action + handleShowIncomingEmail(id, event) { + event?.preventDefault(); + this.send("showIncomingEmail", id); + }, + @observes("filter.{status,user,address,type}") filterEmailLogs() { discourseDebounce(this, this.loadLogs, INPUT_DELAY); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js b/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js index 50d74d35d8..84f85eea6d 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js @@ -1,7 +1,7 @@ import { empty, notEmpty, or } from "@ember/object/computed"; import Controller from "@ember/controller"; import EmailPreview from "admin/models/email-preview"; -import { get } from "@ember/object"; +import { action, get } from "@ember/object"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { inject as service } from "@ember/service"; @@ -14,6 +14,12 @@ export default Controller.extend({ showSendEmailForm: notEmpty("model.html_content"), htmlEmpty: empty("model.html_content"), + @action + toggleShowHtml(event) { + event?.preventDefault(); + this.toggleProperty("showHtml"); + }, + actions: { updateUsername(selected) { this.set("username", get(selected, "firstObject")); @@ -39,10 +45,6 @@ export default Controller.extend({ }); }, - toggleShowHtml() { - this.toggleProperty("showHtml"); - }, - sendEmail() { this.set("sendingEmail", true); this.set("sentEmail", false); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-email-rejected.js b/app/assets/javascripts/admin/addon/controllers/admin-email-rejected.js index 89c67f3cf9..6e4ce78656 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-email-rejected.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-email-rejected.js @@ -3,6 +3,7 @@ import { INPUT_DELAY } from "discourse-common/config/environment"; import IncomingEmail from "admin/models/incoming-email"; import discourseDebounce from "discourse-common/lib/debounce"; import { observes } from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; export default AdminEmailLogsController.extend({ @observes("filter.{status,from,to,subject,error}") @@ -10,6 +11,12 @@ export default AdminEmailLogsController.extend({ discourseDebounce(this, this.loadLogs, IncomingEmail, INPUT_DELAY); }, + @action + handleShowIncomingEmail(id, event) { + event?.preventDefault(); + this.send("showIncomingEmail", id); + }, + actions: { loadMore() { this.loadLogs(IncomingEmail, true); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-logs-screened-ip-addresses.js b/app/assets/javascripts/admin/addon/controllers/admin-logs-screened-ip-addresses.js index 0e8b43f9c1..bc11ec51f4 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-logs-screened-ip-addresses.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-logs-screened-ip-addresses.js @@ -6,6 +6,7 @@ import discourseDebounce from "discourse-common/lib/debounce"; import { exportEntity } from "discourse/lib/export-csv"; import { observes } from "discourse-common/utils/decorators"; import { outputExportResult } from "discourse/lib/export-result"; +import { action } from "@ember/object"; import { inject as service } from "@ember/service"; export default Controller.extend({ @@ -26,6 +27,15 @@ export default Controller.extend({ discourseDebounce(this, this._debouncedShow, INPUT_DELAY); }, + @action + edit(record, event) { + event?.preventDefault(); + if (!record.get("editing")) { + this.set("savedIpAddress", record.get("ip_address")); + } + record.set("editing", true); + }, + actions: { allow(record) { record.set("action_name", "do_nothing"); @@ -37,13 +47,6 @@ export default Controller.extend({ record.save(); }, - edit(record) { - if (!record.get("editing")) { - this.set("savedIpAddress", record.get("ip_address")); - } - record.set("editing", true); - }, - cancel(record) { const savedIpAddress = this.savedIpAddress; if (savedIpAddress && record.get("editing")) { diff --git a/app/assets/javascripts/admin/addon/controllers/admin-logs-staff-action-logs.js b/app/assets/javascripts/admin/addon/controllers/admin-logs-staff-action-logs.js index 670d48c17b..a8a297d47c 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-logs-staff-action-logs.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-logs-staff-action-logs.js @@ -1,5 +1,5 @@ import Controller from "@ember/controller"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { exportEntity } from "discourse/lib/export-csv"; @@ -31,11 +31,13 @@ export default Controller.extend({ this.set( "userHistoryActions", result.extras.user_history_actions - .map((action) => ({ - id: action.id, - action_id: action.action_id, - name: I18n.t("admin.logs.staff_actions.actions." + action.id), - name_raw: action.id, + .map((historyAction) => ({ + id: historyAction.id, + action_id: historyAction.action_id, + name: I18n.t( + "admin.logs.staff_actions.actions." + historyAction.id + ), + name_raw: historyAction.id, })) .sort((a, b) => a.name.localeCompare(b.name)) ); @@ -75,61 +77,74 @@ export default Controller.extend({ this.scheduleRefresh(); }, - actions: { - filterActionIdChanged(filterActionId) { - if (filterActionId) { - this.changeFilters({ - action_name: filterActionId, - action_id: this.userHistoryActions.findBy("id", filterActionId) - .action_id, - }); - } - }, - - clearFilter(key) { - if (key === "actionFilter") { - this.set("filterActionId", null); - this.changeFilters({ - action_name: null, - action_id: null, - custom_type: null, - }); - } else { - this.changeFilters({ [key]: null }); - } - }, - - clearAllFilters() { - this.set("filterActionId", null); - this.resetFilters(); - }, - - filterByAction(logItem) { + @action + filterActionIdChanged(filterActionId) { + if (filterActionId) { this.changeFilters({ - action_name: logItem.get("action_name"), - action_id: logItem.get("action"), - custom_type: logItem.get("custom_type"), + action_name: filterActionId, + action_id: this.userHistoryActions.findBy("id", filterActionId) + .action_id, }); - }, + } + }, - filterByStaffUser(acting_user) { - this.changeFilters({ acting_user: acting_user.username }); - }, + @action + clearFilter(key, event) { + event?.preventDefault(); + if (key === "actionFilter") { + this.set("filterActionId", null); + this.changeFilters({ + action_name: null, + action_id: null, + custom_type: null, + }); + } else { + this.changeFilters({ [key]: null }); + } + }, - filterByTargetUser(target_user) { - this.changeFilters({ target_user: target_user.username }); - }, + @action + clearAllFilters(event) { + event?.preventDefault(); + this.set("filterActionId", null); + this.resetFilters(); + }, - filterBySubject(subject) { - this.changeFilters({ subject }); - }, + @action + filterByAction(logItem, event) { + event?.preventDefault(); + this.changeFilters({ + action_name: logItem.get("action_name"), + action_id: logItem.get("action"), + custom_type: logItem.get("custom_type"), + }); + }, - exportStaffActionLogs() { - exportEntity("staff_action").then(outputExportResult); - }, + @action + filterByStaffUser(acting_user, event) { + event?.preventDefault(); + this.changeFilters({ acting_user: acting_user.username }); + }, - loadMore() { - this.model.loadMore(); - }, + @action + filterByTargetUser(target_user, event) { + event?.preventDefault(); + this.changeFilters({ target_user: target_user.username }); + }, + + @action + filterBySubject(subject, event) { + event?.preventDefault(); + this.changeFilters({ subject }); + }, + + @action + exportStaffActionLogs() { + exportEntity("staff_action").then(outputExportResult); + }, + + @action + loadMore() { + this.model.loadMore(); }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js index 71990f47a3..84b7650ff1 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js @@ -1,5 +1,6 @@ import Controller from "@ember/controller"; import { ajax } from "discourse/lib/ajax"; +import { action } from "@ember/object"; import { alias } from "@ember/object/computed"; import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; @@ -43,6 +44,23 @@ export default Controller.extend({ } }, + @action + showInserted(event) { + event?.preventDefault(); + const webHookId = this.get("model.extras.web_hook_id"); + + ajax(`/admin/api/web_hooks/${webHookId}/events/bulk`, { + type: "GET", + data: { ids: this.incomingEventIds }, + }).then((data) => { + const objects = data.map((webHookEvent) => + this.store.createRecord("web-hook-event", webHookEvent) + ); + this.model.unshiftObjects(objects); + this.set("incomingEventIds", []); + }); + }, + actions: { loadMore() { this.model.loadMore(); @@ -61,20 +79,5 @@ export default Controller.extend({ popupAjaxError(error); }); }, - - showInserted() { - const webHookId = this.get("model.extras.web_hook_id"); - - ajax(`/admin/api/web_hooks/${webHookId}/events/bulk`, { - type: "GET", - data: { ids: this.incomingEventIds }, - }).then((data) => { - const objects = data.map((event) => - this.store.createRecord("web-hook-event", event) - ); - this.model.unshiftObjects(objects); - this.set("incomingEventIds", []); - }); - }, }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-uploaded-image-list.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-uploaded-image-list.js index 7ab4cb6984..8a60037a5b 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-uploaded-image-list.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-uploaded-image-list.js @@ -1,5 +1,6 @@ import { observes, on } from "discourse-common/utils/decorators"; import Controller from "@ember/controller"; +import { action } from "@ember/object"; import ModalFunctionality from "discourse/mixins/modal-functionality"; export default Controller.extend(ModalFunctionality, { @@ -10,15 +11,17 @@ export default Controller.extend(ModalFunctionality, { this.set("images", value && value.length ? value.split("|") : []); }, + @action + remove(url, event) { + event?.preventDefault(); + this.images.removeObject(url); + }, + actions: { uploadDone({ url }) { this.images.addObject(url); }, - remove(url) { - this.images.removeObject(url); - }, - close() { this.save(this.images.join("|")); this.send("closeModal"); diff --git a/app/assets/javascripts/admin/addon/templates/admin-badges/show.hbs b/app/assets/javascripts/admin/addon/templates/admin-badges/show.hbs index 31b16af4cc..82492beecb 100644 --- a/app/assets/javascripts/admin/addon/templates/admin-badges/show.hbs +++ b/app/assets/javascripts/admin/addon/templates/admin-badges/show.hbs @@ -88,9 +88,9 @@
    {{#if this.hasQuery}} - {{i18n "admin.badges.preview.link_text"}} + {{i18n "admin.badges.preview.link_text"}} | - {{i18n "admin.badges.preview.plan_text"}} + {{i18n "admin.badges.preview.plan_text"}} {{#if this.preview_loading}} {{i18n "loading"}} {{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs index d62cfdbd78..998174f20a 100644 --- a/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs @@ -3,7 +3,7 @@ {{#if this.editing}} {{else}} - + {{this.value}} {{/if}} @@ -11,7 +11,7 @@
    {{#if this.editing}} - {{i18n "cancel"}} + {{i18n "cancel"}} {{else}} {{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs index ec8ed70f30..a8c33e1da2 100644 --- a/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs @@ -13,7 +13,7 @@ {{#if this.allowAdvanced}}
  • - @@ -52,7 +52,7 @@ {{else}} - + {{d-icon "plus"}} {{/if}} @@ -61,7 +61,7 @@
  • - + {{d-icon this.maximizeIcon}}
  • diff --git a/app/assets/javascripts/admin/addon/templates/components/ip-lookup.hbs b/app/assets/javascripts/admin/addon/templates/components/ip-lookup.hbs index f63ab16730..31ec7f35a7 100644 --- a/app/assets/javascripts/admin/addon/templates/components/ip-lookup.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/ip-lookup.hbs @@ -3,7 +3,7 @@ {{/if}} {{#if this.show}}
    - {{d-icon "times"}} + {{d-icon "times"}} {{#if this.copied}} {{else}} diff --git a/app/assets/javascripts/admin/addon/templates/components/themes-list-item.hbs b/app/assets/javascripts/admin/addon/templates/components/themes-list-item.hbs index 1710a14454..21678c1a43 100644 --- a/app/assets/javascripts/admin/addon/templates/components/themes-list-item.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/themes-list-item.hbs @@ -31,7 +31,7 @@ {{html-safe this.childrenString}} {{#if this.displayHasMore}} - + {{#if this.childrenExpanded}} {{i18n "admin.customize.theme.collapse"}} {{else}} diff --git a/app/assets/javascripts/admin/addon/templates/email-bounced.hbs b/app/assets/javascripts/admin/addon/templates/email-bounced.hbs index cf6ceb81bc..6a47751d00 100644 --- a/app/assets/javascripts/admin/addon/templates/email-bounced.hbs +++ b/app/assets/javascripts/admin/addon/templates/email-bounced.hbs @@ -30,7 +30,7 @@ {{l.to_address}} {{#if l.has_bounce_key}} - + {{l.email_type}} {{else}} @@ -39,7 +39,7 @@ {{#if l.has_bounce_key}} - + {{d-icon "info-circle"}} {{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/email-preview-digest.hbs b/app/assets/javascripts/admin/addon/templates/email-preview-digest.hbs index d2ee4cad4d..b00332840d 100644 --- a/app/assets/javascripts/admin/addon/templates/email-preview-digest.hbs +++ b/app/assets/javascripts/admin/addon/templates/email-preview-digest.hbs @@ -15,11 +15,11 @@ {{#if this.showHtml}} {{i18n "admin.email.html"}} | - + {{i18n "admin.email.text"}} {{else}} - {{i18n "admin.email.html"}} | + {{i18n "admin.email.html"}} | {{i18n "admin.email.text"}} {{/if}}
    diff --git a/app/assets/javascripts/admin/addon/templates/email-rejected.hbs b/app/assets/javascripts/admin/addon/templates/email-rejected.hbs index 21dcbba21d..19033f5f6f 100644 --- a/app/assets/javascripts/admin/addon/templates/email-rejected.hbs +++ b/app/assets/javascripts/admin/addon/templates/email-rejected.hbs @@ -48,10 +48,10 @@ {{email.subject}} - {{email.error}} + {{email.error}} - + {{d-icon "info-circle"}} diff --git a/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs b/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs index 63489dcdee..ab20b2316b 100644 --- a/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs +++ b/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs @@ -27,7 +27,7 @@ {{#if item.editing}} {{else}} - + {{#if item.isRange}} {{item.ip_address}} {{else}} diff --git a/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs b/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs index bcee25b439..d2e46867c9 100644 --- a/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs +++ b/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs @@ -1,29 +1,29 @@
    {{#if this.filtersExists}} - {{item.actionName}} + {{item.actionName}}
    {{#if item.target_user}} {{avatar item.target_user imageSize="tiny"}} - {{item.target_user.username}} + {{item.target_user.username}} {{/if}} {{#if item.subject}} - {{item.subject}} + {{item.subject}} {{/if}}
    @@ -89,10 +89,10 @@ diff --git a/app/assets/javascripts/admin/addon/templates/modal/admin-uploaded-image-list.hbs b/app/assets/javascripts/admin/addon/templates/modal/admin-uploaded-image-list.hbs index a7aefa7371..aca6fef6f4 100644 --- a/app/assets/javascripts/admin/addon/templates/modal/admin-uploaded-image-list.hbs +++ b/app/assets/javascripts/admin/addon/templates/modal/admin-uploaded-image-list.hbs @@ -1,7 +1,7 @@
    {{#each this.images as |image|}} - + {{bound-avatar-template image "huge"}} {{else}} diff --git a/app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs b/app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs index 2b5c768def..c9791f0fc4 100644 --- a/app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs +++ b/app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs @@ -21,7 +21,7 @@
    {{#if this.hasIncoming}} - + {{/if}} diff --git a/app/assets/javascripts/discourse/app/components/categories-only.js b/app/assets/javascripts/discourse/app/components/categories-only.js index 002ddf82e3..d480f46455 100644 --- a/app/assets/javascripts/discourse/app/components/categories-only.js +++ b/app/assets/javascripts/discourse/app/components/categories-only.js @@ -50,7 +50,8 @@ export default Component.extend({ }, @action - toggleShowMuted() { + toggleShowMuted(event) { + event?.preventDefault(); this.toggleProperty("showMuted"); }, }); diff --git a/app/assets/javascripts/discourse/app/components/category-permission-row.js b/app/assets/javascripts/discourse/app/components/category-permission-row.js index 2e41aa6e63..46eb3690f9 100644 --- a/app/assets/javascripts/discourse/app/components/category-permission-row.js +++ b/app/assets/javascripts/discourse/app/components/category-permission-row.js @@ -1,3 +1,4 @@ +import { action } from "@ember/object"; import { alias, equal } from "@ember/object/computed"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; import Component from "@ember/component"; @@ -92,11 +93,13 @@ export default Component.extend({ this.category.updatePermission(this.group_name, type); }, - actions: { - removeRow() { - this.category.removePermission(this.group_name); - }, + @action + removeRow(event) { + event?.preventDefault(); + this.category.removePermission(this.group_name); + }, + actions: { setPermissionReply() { if (this.type <= PermissionType.CREATE_POST) { this.updatePermission(PermissionType.READONLY); diff --git a/app/assets/javascripts/discourse/app/components/choose-message.js b/app/assets/javascripts/discourse/app/components/choose-message.js index 09d9a9123c..15f0424f60 100644 --- a/app/assets/javascripts/discourse/app/components/choose-message.js +++ b/app/assets/javascripts/discourse/app/components/choose-message.js @@ -1,6 +1,6 @@ import Component from "@ember/component"; import discourseDebounce from "discourse-common/lib/debounce"; -import { get } from "@ember/object"; +import { action, get } from "@ember/object"; import { isEmpty } from "@ember/utils"; import { next } from "@ember/runloop"; import { observes } from "discourse-common/utils/decorators"; @@ -63,12 +63,11 @@ export default Component.extend({ ); }, - actions: { - chooseMessage(message) { - const messageId = get(message, "id"); - this.set("selectedTopicId", messageId); - next(() => $(`#choose-message-${messageId}`).prop("checked", "true")); - return false; - }, + @action + chooseMessage(message, event) { + event?.preventDefault(); + const messageId = get(message, "id"); + this.set("selectedTopicId", messageId); + next(() => $(`#choose-message-${messageId}`).prop("checked", "true")); }, }); diff --git a/app/assets/javascripts/discourse/app/components/composer-messages.js b/app/assets/javascripts/discourse/app/components/composer-messages.js index 40832b5df1..275d1c534c 100644 --- a/app/assets/javascripts/discourse/app/components/composer-messages.js +++ b/app/assets/javascripts/discourse/app/components/composer-messages.js @@ -1,5 +1,5 @@ import Component from "@ember/component"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import LinkLookup from "discourse/lib/link-lookup"; import { not } from "@ember/object/computed"; @@ -54,11 +54,13 @@ export default Component.extend({ this.set("messageCount", messages.get("length")); }, - actions: { - closeMessage(message) { - this._removeMessage(message); - }, + @action + closeMessage(message, event) { + event?.preventDefault(); + this._removeMessage(message); + }, + actions: { hideMessage(message) { this._removeMessage(message); // kind of hacky but the visibility depends on this diff --git a/app/assets/javascripts/discourse/app/components/edit-category-tab.js b/app/assets/javascripts/discourse/app/components/edit-category-tab.js index e96720db36..5619e8c753 100644 --- a/app/assets/javascripts/discourse/app/components/edit-category-tab.js +++ b/app/assets/javascripts/discourse/app/components/edit-category-tab.js @@ -2,6 +2,7 @@ import Component from "@ember/component"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; import { empty } from "@ember/object/computed"; import getURL from "discourse-common/lib/get-url"; import { propertyEqual } from "discourse/lib/computed"; @@ -49,13 +50,12 @@ export default Component.extend({ return getURL(`/c/${slugPart}/edit/${this.tab}`); }, - actions: { - select() { - this.set("selectedTab", this.tab); - - if (!this.newCategory) { - DiscourseURL.routeTo(this.fullSlug); - } - }, + @action + select(event) { + event?.preventDefault(); + this.set("selectedTab", this.tab); + if (!this.newCategory) { + DiscourseURL.routeTo(this.fullSlug); + } }, }); diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker.js b/app/assets/javascripts/discourse/app/components/emoji-picker.js index 3e0646f893..8a5dec83a6 100644 --- a/app/assets/javascripts/discourse/app/components/emoji-picker.js +++ b/app/assets/javascripts/discourse/app/components/emoji-picker.js @@ -240,7 +240,8 @@ export default Component.extend({ }, @action - onCategorySelection(sectionName) { + onCategorySelection(sectionName, event) { + event?.preventDefault(); const section = document.querySelector( `.emoji-picker-emoji-area .section[data-section="${sectionName}"]` ); diff --git a/app/assets/javascripts/discourse/app/components/group-card-contents.js b/app/assets/javascripts/discourse/app/components/group-card-contents.js index cb073a6733..8c7e5d93c5 100644 --- a/app/assets/javascripts/discourse/app/components/group-card-contents.js +++ b/app/assets/javascripts/discourse/app/components/group-card-contents.js @@ -1,3 +1,4 @@ +import { action } from "@ember/object"; import { alias, gt } from "@ember/object/computed"; import CardContentsBase from "discourse/mixins/card-contents-base"; import CleansUp from "discourse/mixins/cleans-up"; @@ -70,11 +71,22 @@ export default Component.extend(CardContentsBase, CleansUp, { this._close(); }, - actions: { - close() { - this._close(); - }, + @action + close(event) { + event?.preventDefault(); + this._close(); + }, + @action + handleShowGroup(group, event) { + event?.preventDefault(); + // Invokes `showGroup` argument. Convert to `this.args.showGroup` when + // refactoring this to a glimmer component. + this.showGroup(group); + this._close(); + }, + + actions: { cancelFilter() { const postStream = this.postStream; postStream.cancelFilter(); @@ -90,8 +102,7 @@ export default Component.extend(CardContentsBase, CleansUp, { }, showGroup(group) { - this.showGroup(group); - this._close(); + this.handleShowGroup(group); }, }, }); diff --git a/app/assets/javascripts/discourse/app/components/group-imap-email-settings.js b/app/assets/javascripts/discourse/app/components/group-imap-email-settings.js index 190a9fb2f9..a437f42d44 100644 --- a/app/assets/javascripts/discourse/app/components/group-imap-email-settings.js +++ b/app/assets/javascripts/discourse/app/components/group-imap-email-settings.js @@ -53,7 +53,8 @@ export default Component.extend({ }, @action - prefillSettings(provider) { + prefillSettings(provider, event) { + event?.preventDefault(); this.form.setProperties(emailProviderDefaultSettings(provider, "imap")); }, diff --git a/app/assets/javascripts/discourse/app/components/group-member.js b/app/assets/javascripts/discourse/app/components/group-member.js index 64b2570dee..08d1097b42 100644 --- a/app/assets/javascripts/discourse/app/components/group-member.js +++ b/app/assets/javascripts/discourse/app/components/group-member.js @@ -1,10 +1,12 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; + export default Component.extend({ classNames: ["item"], - actions: { - remove() { - this.removeAction(this.member); - }, + @action + remove(event) { + event?.preventDefault(); + this.removeAction(this.member); }, }); diff --git a/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js b/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js index 5456c75a79..d4b4bcb9de 100644 --- a/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js +++ b/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js @@ -43,7 +43,8 @@ export default Component.extend({ }, @action - prefillSettings(provider) { + prefillSettings(provider, event) { + event?.preventDefault(); this.form.setProperties(emailProviderDefaultSettings(provider, "smtp")); }, diff --git a/app/assets/javascripts/discourse/app/components/mobile-nav.js b/app/assets/javascripts/discourse/app/components/mobile-nav.js index bdbb2cc85a..f0ea3e6138 100644 --- a/app/assets/javascripts/discourse/app/components/mobile-nav.js +++ b/app/assets/javascripts/discourse/app/components/mobile-nav.js @@ -1,5 +1,6 @@ import { on } from "discourse-common/utils/decorators"; import Component from "@ember/component"; +import { action } from "@ember/object"; import { next } from "@ember/runloop"; import { inject as service } from "@ember/service"; import deprecated from "discourse-common/lib/deprecated"; @@ -56,27 +57,27 @@ export default Component.extend({ this.router.off("routeDidChange", this, this.currentRouteChanged); }, - actions: { - toggleExpanded() { - this.toggleProperty("expanded"); + @action + toggleExpanded(event) { + event?.preventDefault(); + this.toggleProperty("expanded"); - next(() => { - if (this.expanded) { - $(window) - .off("click.mobile-nav") - .on("click.mobile-nav", (e) => { - if (!this.element || this.isDestroying || this.isDestroyed) { - return; - } + next(() => { + if (this.expanded) { + $(window) + .off("click.mobile-nav") + .on("click.mobile-nav", (e) => { + if (!this.element || this.isDestroying || this.isDestroyed) { + return; + } - const expander = this.element.querySelector(".expander"); - if (expander && e.target !== expander) { - this.set("expanded", false); - $(window).off("click.mobile-nav"); - } - }); - } - }); - }, + const expander = this.element.querySelector(".expander"); + if (expander && e.target !== expander) { + this.set("expanded", false); + $(window).off("click.mobile-nav"); + } + }); + } + }); }, }); diff --git a/app/assets/javascripts/discourse/app/components/navigation-bar.js b/app/assets/javascripts/discourse/app/components/navigation-bar.js index ab9a5b9c71..5ede7b2bf4 100644 --- a/app/assets/javascripts/discourse/app/components/navigation-bar.js +++ b/app/assets/javascripts/discourse/app/components/navigation-bar.js @@ -1,5 +1,6 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators"; import Component from "@ember/component"; +import { action } from "@ember/object"; import DiscourseURL from "discourse/lib/url"; import FilterModeMixin from "discourse/mixins/filter-mode"; import { next } from "@ember/runloop"; @@ -61,33 +62,33 @@ export default Component.extend(FilterModeMixin, { DiscourseURL.appEvents.off("dom:clean", this, this.ensureDropClosed); }, - actions: { - toggleDrop() { - this.set("expanded", !this.expanded); + @action + toggleDrop(event) { + event?.preventDefault(); + this.set("expanded", !this.expanded); - if (this.expanded) { - DiscourseURL.appEvents.on("dom:clean", this, this.ensureDropClosed); + if (this.expanded) { + DiscourseURL.appEvents.on("dom:clean", this, this.ensureDropClosed); - next(() => { - if (!this.expanded) { - return; - } + next(() => { + if (!this.expanded) { + return; + } - $(this.element.querySelector(".drop a")).on("click", () => { - this.element.querySelector(".drop").style.display = "none"; + $(this.element.querySelector(".drop a")).on("click", () => { + this.element.querySelector(".drop").style.display = "none"; - next(() => { - this.ensureDropClosed(); - }); - return true; - }); - - $(window).on("click.navigation-bar", () => { + next(() => { this.ensureDropClosed(); - return true; }); + return true; }); - } - }, + + $(window).on("click.navigation-bar", () => { + this.ensureDropClosed(); + return true; + }); + }); + } }, }); diff --git a/app/assets/javascripts/discourse/app/components/reviewable-item.js b/app/assets/javascripts/discourse/app/components/reviewable-item.js index ff67e672a8..73c90d665a 100644 --- a/app/assets/javascripts/discourse/app/components/reviewable-item.js +++ b/app/assets/javascripts/discourse/app/components/reviewable-item.js @@ -7,7 +7,7 @@ import { classify, dasherize } from "@ember/string"; import discourseComputed, { bind } from "discourse-common/utils/decorators"; import optionalService from "discourse/lib/optional-service"; import { popupAjaxError } from "discourse/lib/ajax-error"; -import { set } from "@ember/object"; +import { action, set } from "@ember/object"; import showModal from "discourse/lib/show-modal"; let _components = {}; @@ -121,7 +121,7 @@ export default Component.extend({ }, @bind - _performConfirmed(action) { + _performConfirmed(performableAction) { let reviewable = this.reviewable; let performAction = () => { @@ -140,7 +140,7 @@ export default Component.extend({ }); return ajax( - `/review/${reviewable.id}/perform/${action.id}?version=${version}`, + `/review/${reviewable.id}/perform/${performableAction.id}?version=${version}`, { type: "PUT", data, @@ -173,13 +173,16 @@ export default Component.extend({ .finally(() => this.set("updating", false)); }; - if (action.client_action) { - let actionMethod = this[`client${classify(action.client_action)}`]; + if (performableAction.client_action) { + let actionMethod = + this[`client${classify(performableAction.client_action)}`]; if (actionMethod) { return actionMethod.call(this, reviewable, performAction); } else { // eslint-disable-next-line no-console - console.error(`No handler for ${action.client_action} found`); + console.error( + `No handler for ${performableAction.client_action} found` + ); return; } } else { @@ -209,14 +212,16 @@ export default Component.extend({ } }, - actions: { - explainReviewable(reviewable) { - showModal("explain-reviewable", { - title: "review.explain.title", - model: reviewable, - }); - }, + @action + explainReviewable(reviewable, event) { + event?.preventDefault(); + showModal("explain-reviewable", { + title: "review.explain.title", + model: reviewable, + }); + }, + actions: { edit() { this.set("editing", true); this.set("_updates", { payload: {} }); @@ -259,18 +264,18 @@ export default Component.extend({ set(this._updates, fieldId, event.target.value); }, - perform(action) { + perform(performableAction) { if (this.updating) { return; } - let msg = action.get("confirm_message"); - let requireRejectReason = action.get("require_reject_reason"); - let customModal = action.get("custom_modal"); + let msg = performableAction.get("confirm_message"); + let requireRejectReason = performableAction.get("require_reject_reason"); + let customModal = performableAction.get("custom_modal"); if (msg) { bootbox.confirm(msg, (answer) => { if (answer) { - return this._performConfirmed(action); + return this._performConfirmed(performableAction); } }); } else if (requireRejectReason) { @@ -279,7 +284,7 @@ export default Component.extend({ model: this.reviewable, }).setProperties({ performConfirmed: this._performConfirmed, - action, + action: performableAction, }); } else if (customModal) { showModal(customModal, { @@ -287,10 +292,10 @@ export default Component.extend({ model: this.reviewable, }).setProperties({ performConfirmed: this._performConfirmed, - action, + action: performableAction, }); } else { - return this._performConfirmed(action); + return this._performConfirmed(performableAction); } }, }, diff --git a/app/assets/javascripts/discourse/app/components/reviewable-post-edits.js b/app/assets/javascripts/discourse/app/components/reviewable-post-edits.js index 84c27f152f..75b8692a49 100644 --- a/app/assets/javascripts/discourse/app/components/reviewable-post-edits.js +++ b/app/assets/javascripts/discourse/app/components/reviewable-post-edits.js @@ -1,5 +1,6 @@ import Component from "@ember/component"; import discourseComputed from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; import { gt } from "@ember/object/computed"; import { historyHeat } from "discourse/widgets/post-edits-indicator"; import { longDate } from "discourse/lib/formatter"; @@ -18,18 +19,18 @@ export default Component.extend({ return longDate(updatedAt); }, - actions: { - showEditHistory() { - let postId = this.get("reviewable.post_id"); - this.store.find("post", postId).then((post) => { - let historyController = showModal("history", { - model: post, - modalClass: "history-modal", - }); - historyController.refresh(postId, "latest"); - historyController.set("post", post); - historyController.set("topicController", null); + @action + showEditHistory(event) { + event?.preventDefault(); + let postId = this.get("reviewable.post_id"); + this.store.find("post", postId).then((post) => { + let historyController = showModal("history", { + model: post, + modalClass: "history-modal", }); - }, + historyController.refresh(postId, "latest"); + historyController.set("post", post); + historyController.set("topicController", null); + }); }, }); diff --git a/app/assets/javascripts/discourse/app/components/reviewable-queued-post.js b/app/assets/javascripts/discourse/app/components/reviewable-queued-post.js index e40ab39ef7..d06ddf3c1c 100644 --- a/app/assets/javascripts/discourse/app/components/reviewable-queued-post.js +++ b/app/assets/javascripts/discourse/app/components/reviewable-queued-post.js @@ -1,10 +1,11 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; import showModal from "discourse/lib/show-modal"; export default Component.extend({ - actions: { - showRawEmail() { - showModal("raw-email").set("rawEmail", this.reviewable.payload.raw_email); - }, + @action + showRawEmail(event) { + event?.preventDefault(); + showModal("raw-email").set("rawEmail", this.reviewable.payload.raw_email); }, }); diff --git a/app/assets/javascripts/discourse/app/components/search-result-entry.js b/app/assets/javascripts/discourse/app/components/search-result-entry.js index 9312174ed1..deeaee7825 100644 --- a/app/assets/javascripts/discourse/app/components/search-result-entry.js +++ b/app/assets/javascripts/discourse/app/components/search-result-entry.js @@ -11,6 +11,7 @@ export default Component.extend({ @action logClick(topicId) { + // Important: Don't prevent default handling of clicks if (this.searchLogId && topicId) { logSearchLinkClick({ searchLogId: this.searchLogId, diff --git a/app/assets/javascripts/discourse/app/components/second-factor-form.js b/app/assets/javascripts/discourse/app/components/second-factor-form.js index 3f0c40b381..e208a5f314 100644 --- a/app/assets/javascripts/discourse/app/components/second-factor-form.js +++ b/app/assets/javascripts/discourse/app/components/second-factor-form.js @@ -1,4 +1,5 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; import I18n from "I18n"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; import discourseComputed from "discourse-common/utils/decorators"; @@ -48,15 +49,15 @@ export default Component.extend({ ); }, - actions: { - toggleSecondFactorMethod() { - const secondFactorMethod = this.secondFactorMethod; - this.set("secondFactorToken", ""); - if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) { - this.set("secondFactorMethod", SECOND_FACTOR_METHODS.BACKUP_CODE); - } else { - this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP); - } - }, + @action + toggleSecondFactorMethod(event) { + event?.preventDefault(); + const secondFactorMethod = this.secondFactorMethod; + this.set("secondFactorToken", ""); + if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) { + this.set("secondFactorMethod", SECOND_FACTOR_METHODS.BACKUP_CODE); + } else { + this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP); + } }, }); diff --git a/app/assets/javascripts/discourse/app/components/security-key-form.js b/app/assets/javascripts/discourse/app/components/security-key-form.js index bd6d03f9f4..dcc0725113 100644 --- a/app/assets/javascripts/discourse/app/components/security-key-form.js +++ b/app/assets/javascripts/discourse/app/components/security-key-form.js @@ -1,12 +1,13 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; export default Component.extend({ - actions: { - useAnotherMethod() { - this.set("showSecurityKey", false); - this.set("showSecondFactor", true); - this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP); - }, + @action + useAnotherMethod(event) { + event?.preventDefault(); + this.set("showSecurityKey", false); + this.set("showSecondFactor", true); + this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP); }, }); diff --git a/app/assets/javascripts/discourse/app/components/signup-cta.js b/app/assets/javascripts/discourse/app/components/signup-cta.js index 5ea99dffe9..a4d80fd611 100644 --- a/app/assets/javascripts/discourse/app/components/signup-cta.js +++ b/app/assets/javascripts/discourse/app/components/signup-cta.js @@ -1,15 +1,19 @@ import Component from "@ember/component"; import discourseLater from "discourse-common/lib/later"; +import { action } from "@ember/object"; import { on } from "@ember/object/evented"; export default Component.extend({ action: "showCreateAccount", + @action + neverShow(event) { + event?.preventDefault(); + this.keyValueStore.setItem("anon-cta-never", "t"); + this.session.set("showSignupCta", false); + }, + actions: { - neverShow() { - this.keyValueStore.setItem("anon-cta-never", "t"); - this.session.set("showSignupCta", false); - }, hideForSession() { this.session.set("hideSignupCta", true); this.keyValueStore.setItem("anon-cta-hidden", Date.now()); diff --git a/app/assets/javascripts/discourse/app/components/tag-info.js b/app/assets/javascripts/discourse/app/components/tag-info.js index 6cb2a35dba..21da9a92f1 100644 --- a/app/assets/javascripts/discourse/app/components/tag-info.js +++ b/app/assets/javascripts/discourse/app/components/tag-info.js @@ -7,6 +7,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import { isEmpty } from "@ember/utils"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; export default Component.extend({ dialog: service(), @@ -76,19 +77,49 @@ export default Component.extend({ .catch(popupAjaxError); }, + @action + edit(event) { + event?.preventDefault(); + this.setProperties({ + editing: true, + newTagName: this.tag.id, + newTagDescription: this.tagInfo.description, + }); + }, + + @action + unlinkSynonym(tag, event) { + event?.preventDefault(); + ajax(`/tag/${this.tagInfo.name}/synonyms/${tag.id}`, { + type: "DELETE", + }) + .then(() => this.tagInfo.synonyms.removeObject(tag)) + .catch(popupAjaxError); + }, + + @action + deleteSynonym(tag, event) { + event?.preventDefault(); + bootbox.confirm( + I18n.t("tagging.delete_synonym_confirm", { tag_name: tag.text }), + (result) => { + if (!result) { + return; + } + + tag + .destroyRecord() + .then(() => this.tagInfo.synonyms.removeObject(tag)) + .catch(popupAjaxError); + } + ); + }, + actions: { toggleEditControls() { this.toggleProperty("showEditControls"); }, - edit() { - this.setProperties({ - editing: true, - newTagName: this.tag.id, - newTagDescription: this.tagInfo.description, - }); - }, - cancelEditing() { this.set("editing", false); }, @@ -114,30 +145,6 @@ export default Component.extend({ this.deleteAction(this.tagInfo); }, - unlinkSynonym(tag) { - ajax(`/tag/${this.tagInfo.name}/synonyms/${tag.id}`, { - type: "DELETE", - }) - .then(() => this.tagInfo.synonyms.removeObject(tag)) - .catch(popupAjaxError); - }, - - deleteSynonym(tag) { - bootbox.confirm( - I18n.t("tagging.delete_synonym_confirm", { tag_name: tag.text }), - (result) => { - if (!result) { - return; - } - - tag - .destroyRecord() - .then(() => this.tagInfo.synonyms.removeObject(tag)) - .catch(popupAjaxError); - } - ); - }, - addSynonyms() { bootbox.confirm( I18n.t("tagging.add_synonyms_explanation", { 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 972e824173..2d6ce3e18f 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -251,6 +251,7 @@ export default Component.extend({ if (wantsNewWindow(e)) { return true; } + e.preventDefault(); return this.navigateToTopic(topic, e.target.getAttribute("href")); } @@ -264,6 +265,7 @@ export default Component.extend({ if (wantsNewWindow(e)) { return true; } + e.preventDefault(); return this.navigateToTopic(topic, topic.lastUnreadUrl); } diff --git a/app/assets/javascripts/discourse/app/components/user-card-contents.js b/app/assets/javascripts/discourse/app/components/user-card-contents.js index 930f77a8c1..f114a1abf5 100644 --- a/app/assets/javascripts/discourse/app/components/user-card-contents.js +++ b/app/assets/javascripts/discourse/app/components/user-card-contents.js @@ -1,4 +1,4 @@ -import EmberObject, { set } from "@ember/object"; +import EmberObject, { action, set } from "@ember/object"; import { alias, and, gt, gte, not, or } from "@ember/object/computed"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; import { propertyNotEqual, setting } from "discourse/lib/computed"; @@ -220,6 +220,15 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, { this._close(); }, + @action + handleShowUser(user, event) { + event?.preventDefault(); + // Invokes `showUser` argument. Convert to `this.args.showUser` when + // refactoring this to a glimmer component. + this.showUser(user); + this._close(); + }, + actions: { close() { this._close(); @@ -247,9 +256,8 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, { this._close(); }, - showUser(username) { - this.showUser(username); - this._close(); + showUser(user) { + this.handleShowUser(user); }, checkEmail(user) { diff --git a/app/assets/javascripts/discourse/app/controllers/auth-token.js b/app/assets/javascripts/discourse/app/controllers/auth-token.js index 2d8c91157a..52ddce5f4d 100644 --- a/app/assets/javascripts/discourse/app/controllers/auth-token.js +++ b/app/assets/javascripts/discourse/app/controllers/auth-token.js @@ -1,6 +1,7 @@ import Controller from "@ember/controller"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { ajax } from "discourse/lib/ajax"; +import { action } from "@ember/object"; import { next } from "@ember/runloop"; import { userPath } from "discourse/lib/url"; @@ -17,11 +18,13 @@ export default Controller.extend(ModalFunctionality, { }); }, - actions: { - toggleExpanded() { - this.set("expanded", !this.expanded); - }, + @action + toggleExpanded(event) { + event?.preventDefault(); + this.set("expanded", !this.expanded); + }, + actions: { highlightSecure() { this.send("closeModal"); diff --git a/app/assets/javascripts/discourse/app/controllers/avatar-selector.js b/app/assets/javascripts/discourse/app/controllers/avatar-selector.js index 8373b7a106..d63f464c2a 100644 --- a/app/assets/javascripts/discourse/app/controllers/avatar-selector.js +++ b/app/assets/javascripts/discourse/app/controllers/avatar-selector.js @@ -1,4 +1,5 @@ import Controller from "@ember/controller"; +import { action } from "@ember/object"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { ajax } from "discourse/lib/ajax"; import { allowsImages } from "discourse/lib/uploads"; @@ -132,6 +133,15 @@ export default Controller.extend(ModalFunctionality, { ); }, + @action + selectAvatar(url, event) { + event?.preventDefault(); + this.user + .selectAvatar(url) + .then(() => window.location.reload()) + .catch(popupAjaxError); + }, + actions: { uploadComplete() { this.set("selected", "custom"); @@ -159,13 +169,6 @@ export default Controller.extend(ModalFunctionality, { .finally(() => this.set("gravatarRefreshDisabled", false)); }, - selectAvatar(url) { - this.user - .selectAvatar(url) - .then(() => window.location.reload()) - .catch(popupAjaxError); - }, - saveAvatarSelection() { const selectedUploadId = this.selectedUploadId; const type = this.selected; diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index 3463f6f1d7..61a986d64f 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -508,11 +508,32 @@ export default Controller.extend({ this.set("model.showFullScreenExitPrompt", false); }, - actions: { - togglePreview() { - this.toggleProperty("showPreview"); - }, + @action + async cancel(event) { + event?.preventDefault(); + await this.cancelComposer(); + }, + @action + cancelUpload(event) { + event?.preventDefault(); + this.set("model.uploadCancelled", true); + }, + + @action + togglePreview(event) { + event?.preventDefault(); + this.toggleProperty("showPreview"); + }, + + @action + viewNewReply(event) { + event?.preventDefault(); + DiscourseURL.routeTo(this.get("model.createdPost.url")); + this.close(); + }, + + actions: { closeComposer() { this.close(); }, @@ -543,10 +564,6 @@ export default Controller.extend({ }); }, - cancelUpload() { - this.set("model.uploadCancelled", true); - }, - onPopupMenuAction(menuAction) { this.send(menuAction); }, @@ -707,10 +724,6 @@ export default Controller.extend({ this.set("model.loading", false); }, - async cancel() { - await this.cancelComposer(); - }, - save(ignore, event) { this.save(false, { jump: @@ -1275,12 +1288,6 @@ export default Controller.extend({ } }, - viewNewReply() { - DiscourseURL.routeTo(this.get("model.createdPost.url")); - this.close(); - return false; - }, - async destroyDraft(draftSequence = null) { const key = this.get("model.draftKey"); if (!key) { diff --git a/app/assets/javascripts/discourse/app/controllers/discovery/categories.js b/app/assets/javascripts/discourse/app/controllers/discovery/categories.js index c46f41c1c8..1e07f67cb9 100644 --- a/app/assets/javascripts/discourse/app/controllers/discovery/categories.js +++ b/app/assets/javascripts/discourse/app/controllers/discovery/categories.js @@ -1,5 +1,6 @@ import DiscoveryController from "discourse/controllers/discovery"; import { inject as controller } from "@ember/controller"; +import { action } from "@ember/object"; import { dasherize } from "@ember/string"; import discourseComputed from "discourse-common/utils/decorators"; import { reads } from "@ember/object/computed"; @@ -50,17 +51,19 @@ export default DiscoveryController.extend({ : style; return dasherize(componentName); }, + + @action + showInserted(event) { + event?.preventDefault(); + const tracker = this.topicTrackingState; + // Move inserted into topics + this.model.loadBefore(tracker.get("newIncoming"), true); + tracker.resetTracking(); + }, + actions: { refresh() { this.send("triggerRefresh"); }, - showInserted() { - const tracker = this.topicTrackingState; - - // Move inserted into topics - this.model.loadBefore(tracker.get("newIncoming"), true); - tracker.resetTracking(); - return false; - }, }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/discovery/topics.js b/app/assets/javascripts/discourse/app/controllers/discovery/topics.js index a0037f4da5..3e90fd42a8 100644 --- a/app/assets/javascripts/discourse/app/controllers/discovery/topics.js +++ b/app/assets/javascripts/discourse/app/controllers/discovery/topics.js @@ -63,6 +63,17 @@ const controllerOpts = { return this._isFilterPage(filter, "new") && topicsLength > 0; }, + // Show newly inserted topics + @action + showInserted(event) { + event?.preventDefault(); + const tracker = this.topicTrackingState; + + // Move inserted into topics + this.model.loadBefore(tracker.get("newIncoming"), true); + tracker.resetTracking(); + }, + actions: { changeSort() { deprecated( @@ -72,16 +83,6 @@ const controllerOpts = { return routeAction("changeSort", this.router._router, ...arguments)(); }, - // Show newly inserted topics - showInserted() { - const tracker = this.topicTrackingState; - - // Move inserted into topics - this.model.loadBefore(tracker.get("newIncoming"), true); - tracker.resetTracking(); - return false; - }, - refresh(options = { skipResettingParams: [] }) { const filter = this.get("model.filter"); this.send("resetParams", options.skipResettingParams); diff --git a/app/assets/javascripts/discourse/app/controllers/full-page-search.js b/app/assets/javascripts/discourse/app/controllers/full-page-search.js index e082ca9d29..a7cf017c6a 100644 --- a/app/assets/javascripts/discourse/app/controllers/full-page-search.js +++ b/app/assets/javascripts/discourse/app/controllers/full-page-search.js @@ -14,6 +14,7 @@ import I18n from "I18n"; import { ajax } from "discourse/lib/ajax"; import { escapeExpression } from "discourse/lib/utilities"; import { isEmpty } from "@ember/utils"; +import { action } from "@ember/object"; import { gt, or } from "@ember/object/computed"; import { scrollTop } from "discourse/mixins/scroll-top"; import { setTransient } from "discourse/lib/page-tracker"; @@ -391,22 +392,24 @@ export default Controller.extend({ } }, - actions: { - createTopic(searchTerm) { - let topicCategory; - if (searchTerm.includes("category:")) { - const match = searchTerm.match(/category:(\S*)/); - if (match && match[1]) { - topicCategory = match[1]; - } + @action + createTopic(searchTerm, event) { + event?.preventDefault(); + let topicCategory; + if (searchTerm.includes("category:")) { + const match = searchTerm.match(/category:(\S*)/); + if (match && match[1]) { + topicCategory = match[1]; } - this.composer.open({ - action: Composer.CREATE_TOPIC, - draftKey: Composer.NEW_TOPIC_KEY, - topicCategory, - }); - }, + } + this.composer.open({ + action: Composer.CREATE_TOPIC, + draftKey: Composer.NEW_TOPIC_KEY, + topicCategory, + }); + }, + actions: { selectAll() { this.selected.addObjects(this.get("model.posts").mapBy("topic")); diff --git a/app/assets/javascripts/discourse/app/controllers/history.js b/app/assets/javascripts/discourse/app/controllers/history.js index 2540c78b54..00e64f86da 100644 --- a/app/assets/javascripts/discourse/app/controllers/history.js +++ b/app/assets/javascripts/discourse/app/controllers/history.js @@ -1,3 +1,4 @@ +import { action } from "@ember/object"; import { alias, equal, gt, not, or } from "@ember/object/computed"; import discourseComputed, { observes, @@ -313,6 +314,24 @@ export default Controller.extend(ModalFunctionality, { } }, + @action + displayInline(event) { + event?.preventDefault(); + this.set("viewMode", "inline"); + }, + + @action + displaySideBySide(event) { + event?.preventDefault(); + this.set("viewMode", "side_by_side"); + }, + + @action + displaySideBySideMarkdown(event) { + event?.preventDefault(); + this.set("viewMode", "side_by_side_markdown"); + }, + actions: { loadFirstVersion() { this.refresh(this.get("model.post_id"), this.get("model.first_revision")); @@ -345,15 +364,5 @@ export default Controller.extend(ModalFunctionality, { revertToVersion() { this.revert(this.post, this.get("model.current_revision")); }, - - displayInline() { - this.set("viewMode", "inline"); - }, - displaySideBySide() { - this.set("viewMode", "side_by_side"); - }, - displaySideBySideMarkdown() { - this.set("viewMode", "side_by_side_markdown"); - }, }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js index 1a5a4ecc7f..401c85f354 100644 --- a/app/assets/javascripts/discourse/app/controllers/login.js +++ b/app/assets/javascripts/discourse/app/controllers/login.js @@ -3,7 +3,7 @@ import { alias, not, or, readOnly } from "@ember/object/computed"; import { areCookiesEnabled, escapeExpression } from "discourse/lib/utilities"; import cookie, { removeCookie } from "discourse/lib/cookie"; import { next, schedule } from "@ember/runloop"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; @@ -133,7 +133,66 @@ export default Controller.extend(ModalFunctionality, { return canLoginLocalWithEmail; }, + @action + emailLogin(event) { + event?.preventDefault(); + + if (this.processingEmailLink) { + return; + } + + if (isEmpty(this.loginName)) { + this.flash(I18n.t("login.blank_username"), "info"); + return; + } + + this.set("processingEmailLink", true); + + ajax("/u/email-login", { + data: { login: this.loginName.trim() }, + type: "POST", + }) + .then((data) => { + const loginName = escapeExpression(this.loginName); + const isEmail = loginName.match(/@/); + let key = `email_login.complete_${isEmail ? "email" : "username"}`; + if (data.user_found === false) { + this.flash( + I18n.t(`${key}_not_found`, { + email: loginName, + username: loginName, + }), + "error" + ); + } else { + let postfix = data.hide_taken ? "" : "_found"; + this.flash( + I18n.t(`${key}${postfix}`, { + email: loginName, + username: loginName, + }) + ); + } + }) + .catch((e) => this.flash(extractError(e), "error")) + .finally(() => this.set("processingEmailLink", false)); + }, + + @action + handleForgotPassword(event) { + event?.preventDefault(); + const forgotPasswordController = this.forgotPassword; + if (forgotPasswordController) { + forgotPasswordController.set("accountEmailOrUsername", this.loginName); + } + this.send("showForgotPassword"); + }, + actions: { + forgotPassword() { + this.handleForgotPassword(); + }, + login() { if (this.loginDisabled) { return; @@ -297,56 +356,6 @@ export default Controller.extend(ModalFunctionality, { this.send("showCreateAccount"); }, - forgotPassword() { - const forgotPasswordController = this.forgotPassword; - if (forgotPasswordController) { - forgotPasswordController.set("accountEmailOrUsername", this.loginName); - } - this.send("showForgotPassword"); - }, - - emailLogin() { - if (this.processingEmailLink) { - return; - } - - if (isEmpty(this.loginName)) { - this.flash(I18n.t("login.blank_username"), "info"); - return; - } - - this.set("processingEmailLink", true); - - ajax("/u/email-login", { - data: { login: this.loginName.trim() }, - type: "POST", - }) - .then((data) => { - const loginName = escapeExpression(this.loginName); - const isEmail = loginName.match(/@/); - let key = `email_login.complete_${isEmail ? "email" : "username"}`; - if (data.user_found === false) { - this.flash( - I18n.t(`${key}_not_found`, { - email: loginName, - username: loginName, - }), - "error" - ); - } else { - let postfix = data.hide_taken ? "" : "_found"; - this.flash( - I18n.t(`${key}${postfix}`, { - email: loginName, - username: loginName, - }) - ); - } - }) - .catch((e) => this.flash(extractError(e), "error")) - .finally(() => this.set("processingEmailLink", false)); - }, - authenticateSecurityKey() { getWebauthnCredential( this.securityKeyChallenge, diff --git a/app/assets/javascripts/discourse/app/controllers/password-reset.js b/app/assets/javascripts/discourse/app/controllers/password-reset.js index bae85083a8..0787a28271 100644 --- a/app/assets/javascripts/discourse/app/controllers/password-reset.js +++ b/app/assets/javascripts/discourse/app/controllers/password-reset.js @@ -1,4 +1,5 @@ import DiscourseURL, { userPath } from "discourse/lib/url"; +import { action } from "@ember/object"; import { alias, or, readOnly } from "@ember/object/computed"; import Controller from "@ember/controller"; import I18n from "I18n"; @@ -46,6 +47,13 @@ export default Controller.extend(PasswordValidation, { lockImageUrl: getURL("/images/lock.svg"), + @action + done(event) { + event?.preventDefault(); + this.set("redirected", true); + DiscourseURL.redirectTo(this.redirectTo || "/"); + }, + actions: { submit() { ajax({ @@ -126,10 +134,5 @@ export default Controller.extend(PasswordValidation, { } ); }, - - done() { - this.set("redirected", true); - DiscourseURL.redirectTo(this.redirectTo || "/"); - }, }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/account.js b/app/assets/javascripts/discourse/app/controllers/preferences/account.js index 6c3051d452..94ba197cfd 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/account.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/account.js @@ -2,7 +2,7 @@ import { gt, not, or } from "@ember/object/computed"; import { propertyNotEqual, setting } from "discourse/lib/computed"; import CanCheckEmails from "discourse/mixins/can-check-emails"; import Controller from "@ember/controller"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { findAll } from "discourse/models/login-method"; @@ -132,6 +132,20 @@ export default Controller.extend(CanCheckEmails, { return findAll().length > 0; }, + @action + resendConfirmationEmail(email, event) { + event?.preventDefault(); + email.set("resending", true); + this.model + .addEmail(email.email) + .then(() => { + email.set("resent", true); + }) + .finally(() => { + email.set("resending", false); + }); + }, + actions: { save() { this.set("saved", false); @@ -157,18 +171,6 @@ export default Controller.extend(CanCheckEmails, { this.model.destroyEmail(email); }, - resendConfirmationEmail(email) { - email.set("resending", true); - this.model - .addEmail(email.email) - .then(() => { - email.set("resent", true); - }) - .finally(() => { - email.set("resending", false); - }); - }, - delete() { this.dialog.alert({ message: I18n.t("user.delete_account_confirm"), diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js b/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js index 946903a568..fc5293054a 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js @@ -3,6 +3,7 @@ import CanCheckEmails from "discourse/mixins/can-check-emails"; import Controller from "@ember/controller"; import I18n from "I18n"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; +import { action } from "@ember/object"; import { alias } from "@ember/object/computed"; import bootbox from "bootbox"; import discourseComputed from "discourse-common/utils/decorators"; @@ -91,6 +92,27 @@ export default Controller.extend(CanCheckEmails, { this.set("dirty", true); }, + @action + resetPassword(event) { + event?.preventDefault(); + + this.setProperties({ + resetPasswordLoading: true, + resetPasswordProgress: "", + }); + + return this.model + .changePassword() + .then(() => { + this.set( + "resetPasswordProgress", + I18n.t("user.change_password.success") + ); + }) + .catch(popupAjaxError) + .finally(() => this.set("resetPasswordLoading", false)); + }, + actions: { confirmPassword() { if (!this.password) { @@ -101,24 +123,6 @@ export default Controller.extend(CanCheckEmails, { this.set("password", null); }, - resetPassword() { - this.setProperties({ - resetPasswordLoading: true, - resetPasswordProgress: "", - }); - - return this.model - .changePassword() - .then(() => { - this.set( - "resetPasswordProgress", - I18n.t("user.change_password.success") - ); - }) - .catch(popupAjaxError) - .finally(() => this.set("resetPasswordLoading", false)); - }, - disableAllSecondFactors() { if (this.loading) { return; diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/security.js b/app/assets/javascripts/discourse/app/controllers/preferences/security.js index c1e7dc52c0..800d597c65 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/security.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/security.js @@ -1,4 +1,5 @@ import Controller from "@ember/controller"; +import { action } from "@ember/object"; import { gt } from "@ember/object/computed"; import discourseComputed from "discourse-common/utils/decorators"; import { ajax } from "discourse/lib/ajax"; @@ -51,6 +52,56 @@ export default Controller.extend(CanCheckEmails, { DEFAULT_AUTH_TOKENS_COUNT ), + @action + changePassword(event) { + event?.preventDefault(); + if (!this.passwordProgress) { + this.set("passwordProgress", I18n.t("user.change_password.in_progress")); + return this.model + .changePassword() + .then(() => { + // password changed + this.setProperties({ + changePasswordProgress: false, + passwordProgress: I18n.t("user.change_password.success"), + }); + }) + .catch(() => { + // password failed to change + this.setProperties({ + changePasswordProgress: false, + passwordProgress: I18n.t("user.change_password.error"), + }); + }); + } + }, + + @action + toggleShowAllAuthTokens(event) { + event?.preventDefault(); + this.toggleProperty("showAllAuthTokens"); + }, + + @action + revokeAuthToken(token, event) { + event?.preventDefault(); + ajax( + userPath( + `${this.get("model.username_lower")}/preferences/revoke-auth-token` + ), + { + type: "POST", + data: token ? { token_id: token.id } : {}, + } + ) + .then(() => { + if (!token) { + logout(); + } // All sessions revoked + }) + .catch(popupAjaxError); + }, + actions: { save() { this.set("saved", false); @@ -60,53 +111,6 @@ export default Controller.extend(CanCheckEmails, { .catch(popupAjaxError); }, - changePassword() { - if (!this.passwordProgress) { - this.set( - "passwordProgress", - I18n.t("user.change_password.in_progress") - ); - return this.model - .changePassword() - .then(() => { - // password changed - this.setProperties({ - changePasswordProgress: false, - passwordProgress: I18n.t("user.change_password.success"), - }); - }) - .catch(() => { - // password failed to change - this.setProperties({ - changePasswordProgress: false, - passwordProgress: I18n.t("user.change_password.error"), - }); - }); - } - }, - - toggleShowAllAuthTokens() { - this.toggleProperty("showAllAuthTokens"); - }, - - revokeAuthToken(token) { - ajax( - userPath( - `${this.get("model.username_lower")}/preferences/revoke-auth-token` - ), - { - type: "POST", - data: token ? { token_id: token.id } : {}, - } - ) - .then(() => { - if (!token) { - logout(); - } // All sessions revoked - }) - .catch(popupAjaxError); - }, - showToken(token) { showModal("auth-token", { model: token }); }, diff --git a/app/assets/javascripts/discourse/app/controllers/second-factor-add-totp.js b/app/assets/javascripts/discourse/app/controllers/second-factor-add-totp.js index bdc1f3511d..2ed9127535 100644 --- a/app/assets/javascripts/discourse/app/controllers/second-factor-add-totp.js +++ b/app/assets/javascripts/discourse/app/controllers/second-factor-add-totp.js @@ -1,4 +1,5 @@ import Controller from "@ember/controller"; +import { action } from "@ember/object"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; @@ -40,9 +41,15 @@ export default Controller.extend(ModalFunctionality, { .finally(() => this.set("loading", false)); }, + @action + enableShowSecondFactorKey(event) { + event?.preventDefault(); + this.set("showSecondFactorKey", true); + }, + actions: { showSecondFactorKey() { - this.set("showSecondFactorKey", true); + this.enableShowSecondFactorKey(); }, enableSecondFactor() { diff --git a/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js b/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js index 8e88afdf95..f4199f9ee9 100644 --- a/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js +++ b/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js @@ -212,7 +212,8 @@ export default Controller.extend({ }, @action - useAnotherMethod(newMethod) { + useAnotherMethod(newMethod, event) { + event?.preventDefault(); this.set("userSelectedMethod", newMethod); }, diff --git a/app/assets/javascripts/discourse/app/controllers/tag-show.js b/app/assets/javascripts/discourse/app/controllers/tag-show.js index 619c5e860d..2f9c493516 100644 --- a/app/assets/javascripts/discourse/app/controllers/tag-show.js +++ b/app/assets/javascripts/discourse/app/controllers/tag-show.js @@ -110,7 +110,8 @@ export default DiscoverySortableController.extend( }, @action - showInserted() { + showInserted(event) { + event?.preventDefault(); const tracker = this.topicTrackingState; this.list.loadBefore(tracker.newIncoming, true); tracker.resetTracking(); diff --git a/app/assets/javascripts/discourse/app/controllers/tags-index.js b/app/assets/javascripts/discourse/app/controllers/tags-index.js index ec56d24003..eff2f1d30b 100644 --- a/app/assets/javascripts/discourse/app/controllers/tags-index.js +++ b/app/assets/javascripts/discourse/app/controllers/tags-index.js @@ -1,3 +1,4 @@ +import { action } from "@ember/object"; import { alias, notEmpty } from "@ember/object/computed"; import Controller from "@ember/controller"; import I18n from "I18n"; @@ -41,23 +42,27 @@ export default Controller.extend({ }; }, + @action + sortByCount(event) { + event?.preventDefault(); + this.setProperties({ + sortProperties: ["totalCount:desc", "id"], + sortedByCount: true, + sortedByName: false, + }); + }, + + @action + sortById(event) { + event?.preventDefault(); + this.setProperties({ + sortProperties: ["id"], + sortedByCount: false, + sortedByName: true, + }); + }, + actions: { - sortByCount() { - this.setProperties({ - sortProperties: ["totalCount:desc", "id"], - sortedByCount: true, - sortedByName: false, - }); - }, - - sortById() { - this.setProperties({ - sortProperties: ["id"], - sortedByCount: false, - sortedByName: true, - }); - }, - showUploader() { showModal("tag-upload"); }, diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index aace5530cb..913362255e 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -313,6 +313,55 @@ export default Controller.extend(bufferedProperty("model"), { }); }, + @action + editTopic(event) { + event?.preventDefault(); + if (this.get("model.details.can_edit")) { + this.set("editingTopic", true); + } + }, + + @action + jumpTop(event) { + event?.preventDefault(); + DiscourseURL.routeTo(this.get("model.firstPostUrl"), { + skipIfOnScreen: false, + keepFilter: true, + }); + }, + + @action + removeFeaturedLink(event) { + event?.preventDefault(); + this.set("buffered.featured_link", null); + }, + + @action + selectAll(event) { + event?.preventDefault(); + const smallActionsPostIds = this._smallActionPostIds(); + this.set("selectedPostIds", [ + ...this.get("model.postStream.stream").filter( + (postId) => !smallActionsPostIds.has(postId) + ), + ]); + this._forceRefreshPostStream(); + }, + + @action + deselectAll(event) { + event?.preventDefault(); + this.set("selectedPostIds", []); + this._forceRefreshPostStream(); + }, + + @action + toggleMultiSelect(event) { + event?.preventDefault(); + this.toggleProperty("multiSelect"); + this._forceRefreshPostStream(); + }, + actions: { topicCategoryChanged(categoryId) { this.set("buffered.category_id", categoryId); @@ -822,13 +871,6 @@ export default Controller.extend(bufferedProperty("model"), { this._jumpToPostNumber(postNumber); }, - jumpTop() { - DiscourseURL.routeTo(this.get("model.firstPostUrl"), { - skipIfOnScreen: false, - keepFilter: true, - }); - }, - jumpBottom() { // When a topic only has one lengthy post const jumpEnd = this.model.highest_post_number === 1 ? true : false; @@ -859,26 +901,6 @@ export default Controller.extend(bufferedProperty("model"), { this._jumpToPostId(postId); }, - toggleMultiSelect() { - this.toggleProperty("multiSelect"); - this._forceRefreshPostStream(); - }, - - selectAll() { - const smallActionsPostIds = this._smallActionPostIds(); - this.set("selectedPostIds", [ - ...this.get("model.postStream.stream").filter( - (postId) => !smallActionsPostIds.has(postId) - ), - ]); - this._forceRefreshPostStream(); - }, - - deselectAll() { - this.set("selectedPostIds", []); - this._forceRefreshPostStream(); - }, - togglePostSelection(post) { const selected = this.selectedPostIds; selected.includes(post.id) @@ -973,13 +995,6 @@ export default Controller.extend(bufferedProperty("model"), { .then(() => this.updateQueryParams); }, - editTopic() { - if (this.get("model.details.can_edit")) { - this.set("editingTopic", true); - } - return false; - }, - cancelEditingTopic() { this.set("editingTopic", false); this.rollbackBuffer(); @@ -1159,10 +1174,6 @@ export default Controller.extend(bufferedProperty("model"), { .catch(popupAjaxError); }, - removeFeaturedLink() { - this.set("buffered.featured_link", null); - }, - resetBumpDate() { this.model.resetBumpDate(); }, diff --git a/app/assets/javascripts/discourse/app/controllers/user-private-messages-tags.js b/app/assets/javascripts/discourse/app/controllers/user-private-messages-tags.js index 9d4f536866..b73149358f 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-private-messages-tags.js +++ b/app/assets/javascripts/discourse/app/controllers/user-private-messages-tags.js @@ -1,25 +1,29 @@ import Controller from "@ember/controller"; +import { action } from "@ember/object"; + export default Controller.extend({ sortProperties: ["count:desc", "id"], tagsForUser: null, sortedByCount: true, sortedByName: false, - actions: { - sortByCount() { - this.setProperties({ - sortProperties: ["count:desc", "id"], - sortedByCount: true, - sortedByName: false, - }); - }, + @action + sortByCount(event) { + event?.preventDefault(); + this.setProperties({ + sortProperties: ["count:desc", "id"], + sortedByCount: true, + sortedByName: false, + }); + }, - sortById() { - this.setProperties({ - sortProperties: ["id"], - sortedByCount: false, - sortedByName: true, - }); - }, + @action + sortById(event) { + event?.preventDefault(); + this.setProperties({ + sortProperties: ["id"], + sortedByCount: false, + sortedByName: true, + }); }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/user-topics-list.js b/app/assets/javascripts/discourse/app/controllers/user-topics-list.js index 021502f615..a40ab97f4e 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-topics-list.js +++ b/app/assets/javascripts/discourse/app/controllers/user-topics-list.js @@ -81,10 +81,10 @@ export default Controller.extend(BulkTopicSelection, { }, @action - showInserted() { + showInserted(event) { + event?.preventDefault(); this.model.loadBefore(this.pmTopicTrackingState.newIncoming); this.pmTopicTrackingState.resetIncomingTracking(); - return false; }, @action diff --git a/app/assets/javascripts/discourse/app/controllers/user.js b/app/assets/javascripts/discourse/app/controllers/user.js index 4c8ba40cbc..8f2d24beb1 100644 --- a/app/assets/javascripts/discourse/app/controllers/user.js +++ b/app/assets/javascripts/discourse/app/controllers/user.js @@ -211,6 +211,15 @@ export default Controller.extend(CanCheckEmails, { } }, + @action + showSuspensions(event) { + event?.preventDefault(); + this.adminTools.showActionLogs(this, { + target_user: this.get("model.username"), + action_name: "suspend_user", + }); + }, + actions: { collapseProfile() { this.set("forceExpand", false); @@ -220,13 +229,6 @@ export default Controller.extend(CanCheckEmails, { this.set("forceExpand", true); }, - showSuspensions() { - this.adminTools.showActionLogs(this, { - target_user: this.get("model.username"), - action_name: "suspend_user", - }); - }, - adminDelete() { const userId = this.get("model.id"); const location = document.location.pathname; diff --git a/app/assets/javascripts/discourse/app/templates/components/categories-only.hbs b/app/assets/javascripts/discourse/app/templates/components/categories-only.hbs index 338e4b1cf5..6f3be0d4a9 100644 --- a/app/assets/javascripts/discourse/app/templates/components/categories-only.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/categories-only.hbs @@ -20,7 +20,7 @@ {{#if this.mutedCategories}}
    - +

    {{i18n "categories.muted"}}

    {{#if this.mutedToggleIcon}} {{d-icon this.mutedToggleIcon}} diff --git a/app/assets/javascripts/discourse/app/templates/components/category-permission-row.hbs b/app/assets/javascripts/discourse/app/templates/components/category-permission-row.hbs index 16e062fda2..4acbfc6298 100644 --- a/app/assets/javascripts/discourse/app/templates/components/category-permission-row.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/category-permission-row.hbs @@ -1,6 +1,6 @@ {{this.group_name}} -
    + {{d-icon "far-trash-alt"}} diff --git a/app/assets/javascripts/discourse/app/templates/components/choose-message.hbs b/app/assets/javascripts/discourse/app/templates/components/choose-message.hbs index ce6c36f17d..c7d07e87f2 100644 --- a/app/assets/javascripts/discourse/app/templates/components/choose-message.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/choose-message.hbs @@ -11,7 +11,7 @@ {{#each this.messages as |m|}}
    {{/if}} -{{/unless}} +{{/if}}
    {{outlet}} diff --git a/app/assets/javascripts/discourse/app/templates/user/notifications.hbs b/app/assets/javascripts/discourse/app/templates/user/notifications.hbs index 462340cf05..a1ba2bbf29 100644 --- a/app/assets/javascripts/discourse/app/templates/user/notifications.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/notifications.hbs @@ -35,7 +35,7 @@ - + {{#if this.model}} From c0037dc0f06f08bd050aedc8aad97d1f05b322ab Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Wed, 5 Oct 2022 14:20:03 +0800 Subject: [PATCH 050/332] FIX: Missing sidebar section link icon for PM tags (#18481) No tests cause a missing icon regression is not the end of the world and I don't feel like it will provide enough value as a regression test long term. --- .../messages-section/message-section-link.js | 8 ++----- .../tags-section/base-tag-section-link.js | 21 +++++++++++++++++ .../user/tags-section/pm-tag-section-link.js | 16 ++++--------- .../user/tags-section/tag-section-link.js | 23 ++++--------------- 4 files changed, 32 insertions(+), 36 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/base-tag-section-link.js diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/messages-section/message-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/messages-section/message-section-link.js index a28cebdeb7..d1a10fefb0 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/messages-section/message-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/messages-section/message-section-link.js @@ -73,14 +73,10 @@ export default class MessageSectionLink { } get prefixType() { - if (this._isInbox) { - return "icon"; - } + return "icon"; } get prefixValue() { - if (this._isInbox) { - return "inbox"; - } + return "inbox"; } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/base-tag-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/base-tag-section-link.js new file mode 100644 index 0000000000..6a32ae9027 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/base-tag-section-link.js @@ -0,0 +1,21 @@ +export default class BaseTagSectionLink { + constructor({ tagName }) { + this.tagName = tagName; + } + + get name() { + return this.tagName; + } + + get text() { + return this.tagName; + } + + get prefixType() { + return "icon"; + } + + get prefixValue() { + return "tag"; + } +} diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/pm-tag-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/pm-tag-section-link.js index fceac26abf..3c1b4ccb1b 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/pm-tag-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/pm-tag-section-link.js @@ -1,11 +1,9 @@ -export default class PMTagSectionLink { - constructor({ tagName, currentUser }) { - this.tagName = tagName; - this.currentUser = currentUser; - } +import BaseTagSectionLink from "discourse/lib/sidebar/user/tags-section/base-tag-section-link"; - get name() { - return this.tagName; +export default class PMTagSectionLink extends BaseTagSectionLink { + constructor({ currentUser }) { + super(...arguments); + this.currentUser = currentUser; } get models() { @@ -15,8 +13,4 @@ export default class PMTagSectionLink { get route() { return "userPrivateMessages.tagsShow"; } - - get text() { - return this.tagName; - } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/tag-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/tag-section-link.js index 85ee3ae956..094fe74de2 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/tag-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/tag-section-link.js @@ -3,13 +3,14 @@ import I18n from "I18n"; import { tracked } from "@glimmer/tracking"; import { bind } from "discourse-common/utils/decorators"; +import BaseTagSectionLink from "discourse/lib/sidebar/user/tags-section/base-tag-section-link"; -export default class TagSectionLink { +export default class TagSectionLink extends BaseTagSectionLink { @tracked totalUnread = 0; @tracked totalNew = 0; - constructor({ tagName, topicTrackingState }) { - this.tagName = tagName; + constructor({ topicTrackingState }) { + super(...arguments); this.topicTrackingState = topicTrackingState; this.refreshCounts(); } @@ -27,10 +28,6 @@ export default class TagSectionLink { } } - get name() { - return this.tagName; - } - get models() { return [this.tagName]; } @@ -49,10 +46,6 @@ export default class TagSectionLink { return "tag.show tag.showNew tag.showUnread tag.showTop"; } - get text() { - return this.tagName; - } - get badgeText() { if (this.totalUnread > 0) { return I18n.t("sidebar.unread_count", { @@ -64,12 +57,4 @@ export default class TagSectionLink { }); } } - - get prefixType() { - return "icon"; - } - - get prefixValue() { - return "tag"; - } } From 4d05e3edab1c2968980316ef6e1d0ef7e5bdca82 Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Wed, 5 Oct 2022 12:30:02 +0300 Subject: [PATCH 051/332] DEV: Include pending reviewables in the main tab in the user menu (#18471) This commit makes pending reviewables show up in the main tab (a.k.a. "all notifications" tab). Pending reviewables along with unread notifications are always shown first and they're sorted based on their creation date (most recent comes first). The dismiss button currently only shows up if there are unread notifications and it doesn't dismiss pending reviewables. We may follow up with another change soon that allows makes the dismiss button work with reviewables and remove them from the list without taking any action on them. Follow-up to https://github.com/discourse/discourse/commit/079450c9e496ac9bed8ff491ceb1e4ac0b83448f. --- .../app/components/user-menu/messages-list.js | 48 ++---- .../user-menu/notifications-list.js | 80 ++++++++-- .../discourse/app/lib/utilities.js | 26 ++++ .../discourse/app/models/notification.js | 6 + .../user-menu/notifications-list-test.js | 124 ++++++++++++++- .../tests/unit/lib/utilities-test.js | 40 +++++ app/controllers/notifications_controller.rb | 29 ++-- app/controllers/reviewables_controller.rb | 6 +- app/models/reviewable.rb | 8 + .../basic_reviewable_serializer.rb | 2 +- .../requests/notifications_controller_spec.rb | 142 +++++++++++------- .../common_basic_reviewable_serializer.rb | 8 + 12 files changed, 394 insertions(+), 125 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js b/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js index 7d916ec668..192967c3ca 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js @@ -6,18 +6,7 @@ import I18n from "I18n"; import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item"; import UserMenuMessageItem from "discourse/lib/user-menu/message-item"; import Topic from "discourse/models/topic"; - -function parseDateString(date) { - if (date) { - return new Date(date); - } -} - -async function initializeNotifications(rawList) { - const notifications = rawList.map((n) => Notification.create(n)); - await Notification.applyTransformations(notifications); - return notifications; -} +import { mergeSortedLists } from "discourse/lib/utilities"; export default class UserMenuMessagesList extends UserMenuNotificationsList { get dismissTypes() { @@ -63,7 +52,7 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList { ); const content = []; - const unreadNotifications = await initializeNotifications( + const unreadNotifications = await Notification.initializeNotifications( data.unread_notifications ); unreadNotifications.forEach((notification) => { @@ -80,38 +69,29 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList { const topics = data.topics.map((t) => Topic.create(t)); await Topic.applyTransformations(topics); - const readNotifications = await initializeNotifications( + const readNotifications = await Notification.initializeNotifications( data.read_notifications ); - let latestReadNotificationDate = parseDateString( - readNotifications[0]?.created_at - ); - let latestMessageDate = parseDateString(topics[0]?.bumped_at); - - while (latestReadNotificationDate || latestMessageDate) { - if ( - !latestReadNotificationDate || - (latestMessageDate && latestReadNotificationDate < latestMessageDate) - ) { - content.push(new UserMenuMessageItem({ message: topics[0] })); - topics.shift(); - latestMessageDate = parseDateString(topics[0]?.bumped_at); - } else { + mergeSortedLists(readNotifications, topics, (notification, topic) => { + const notificationCreatedAt = new Date(notification.created_at); + const topicBumpedAt = new Date(topic.bumped_at); + return topicBumpedAt > notificationCreatedAt; + }).forEach((item) => { + if (item instanceof Notification) { content.push( new UserMenuNotificationItem({ - notification: readNotifications[0], + notification: item, currentUser: this.currentUser, siteSettings: this.siteSettings, site: this.site, }) ); - readNotifications.shift(); - latestReadNotificationDate = parseDateString( - readNotifications[0]?.created_at - ); + } else { + content.push(new UserMenuMessageItem({ message: item })); } - } + }); + return content; } diff --git a/app/assets/javascripts/discourse/app/components/user-menu/notifications-list.js b/app/assets/javascripts/discourse/app/components/user-menu/notifications-list.js index a585b5a06f..efe1cadf52 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/notifications-list.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/notifications-list.js @@ -2,11 +2,16 @@ import UserMenuItemsList from "discourse/components/user-menu/items-list"; import I18n from "I18n"; import { action } from "@ember/object"; import { ajax } from "discourse/lib/ajax"; -import { postRNWebviewMessage } from "discourse/lib/utilities"; +import { + mergeSortedLists, + postRNWebviewMessage, +} from "discourse/lib/utilities"; import showModal from "discourse/lib/show-modal"; import { inject as service } from "@ember/service"; import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item"; import Notification from "discourse/models/notification"; +import UserMenuReviewable from "discourse/models/user-menu-reviewable"; +import UserMenuReviewableItem from "discourse/lib/user-menu/reviewable-item"; export default class UserMenuNotificationsList extends UserMenuItemsList { @service currentUser; @@ -31,7 +36,11 @@ export default class UserMenuNotificationsList extends UserMenuItemsList { } get showDismiss() { - return this.items.some((item) => !item.notification.read); + return Object.keys( + this.currentUser.get("grouped_unread_notifications") || {} + ).any((key) => { + return this.currentUser.get(`grouped_unread_notifications.${key}`) > 0; + }); } get dismissTitle() { @@ -60,27 +69,70 @@ export default class UserMenuNotificationsList extends UserMenuItemsList { limit: 30, recent: true, bump_last_seen_reviewable: true, - silent: this.currentUser.enforcedSecondFactor, }; + if (this.currentUser.enforcedSecondFactor) { + params.silent = true; + } + const types = this.filterByTypes; if (types?.length > 0) { params.filter_by_types = types.join(","); params.silent = true; } - const collection = await this.store - .findStale("notification", params) - .refresh(); - const notifications = collection.content; - await Notification.applyTransformations(notifications); - return notifications.map((notification) => { - return new UserMenuNotificationItem({ - notification, - currentUser: this.currentUser, - siteSettings: this.siteSettings, - site: this.site, + + const content = []; + const data = await ajax("/notifications", { data: params }); + + const notifications = await Notification.initializeNotifications( + data.notifications + ); + + const reviewables = data.pending_reviewables?.map((r) => + UserMenuReviewable.create(r) + ); + + if (reviewables?.length) { + const firstReadNotificationIndex = notifications.findIndex((n) => n.read); + const unreadNotifications = notifications.splice( + 0, + firstReadNotificationIndex + ); + mergeSortedLists( + unreadNotifications, + reviewables, + (notification, reviewable) => { + const notificationCreatedAt = new Date(notification.created_at); + const reviewableCreatedAt = new Date(reviewable.created_at); + return reviewableCreatedAt > notificationCreatedAt; + } + ).forEach((item) => { + const props = { + currentUser: this.currentUser, + siteSettings: this.siteSettings, + site: this.site, + }; + if (item instanceof Notification) { + props.notification = item; + content.push(new UserMenuNotificationItem(props)); + } else { + props.reviewable = item; + content.push(new UserMenuReviewableItem(props)); + } }); + } + + notifications.forEach((notification) => { + content.push( + new UserMenuNotificationItem({ + notification, + currentUser: this.currentUser, + siteSettings: this.siteSettings, + site: this.site, + }) + ); }); + return content; } dismissWarningModal() { diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js index a54b3c6c67..d2ae39cdea 100644 --- a/app/assets/javascripts/discourse/app/lib/utilities.js +++ b/app/assets/javascripts/discourse/app/lib/utilities.js @@ -606,5 +606,31 @@ function clipboardCopyFallback(text) { return success; } +// this function takes 2 sorted lists and returns another sorted list that +// contains both of the original lists. +// you need to provide a callback as the 3rd argument that will be called with +// an item from the first list (1st callback argument) and another item from +// the second list (2nd callback argument). The callback should return true if +// its 2nd argument should go before its 1st argument and return false +// otherwise. +export function mergeSortedLists(list1, list2, comparator) { + let index1 = 0; + let index2 = 0; + const merged = []; + while (index1 < list1.length || index2 < list2.length) { + if ( + index1 === list1.length || + (index2 < list2.length && comparator(list1[index1], list2[index2])) + ) { + merged.push(list2[index2]); + index2++; + } else { + merged.push(list1[index1]); + index1++; + } + } + return merged; +} + // This prevents a mini racer crash export default {}; diff --git a/app/assets/javascripts/discourse/app/models/notification.js b/app/assets/javascripts/discourse/app/models/notification.js index 000aa0c10a..b49d8003c2 100644 --- a/app/assets/javascripts/discourse/app/models/notification.js +++ b/app/assets/javascripts/discourse/app/models/notification.js @@ -7,5 +7,11 @@ export default class Notification extends RestModel { await applyModelTransformations("notification", notifications); } + static async initializeNotifications(rawList) { + const notifications = rawList.map((n) => this.create(n)); + await this.applyTransformations(notifications); + return notifications; + } + @tracked read; } diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/notifications-list-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/notifications-list-test.js index cf1baf30b8..1c8c6e4fd8 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/notifications-list-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/user-menu/notifications-list-test.js @@ -1,10 +1,11 @@ import { module, test } from "qunit"; import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { exists, query } from "discourse/tests/helpers/qunit-helpers"; +import { exists, query, queryAll } from "discourse/tests/helpers/qunit-helpers"; import { click, render } from "@ember/test-helpers"; import { cloneJSON } from "discourse-common/lib/object"; import NotificationFixtures from "discourse/tests/fixtures/notification-fixtures"; import { hbs } from "ember-cli-htmlbars"; +import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types"; import pretender, { response } from "discourse/tests/helpers/create-pretender"; import I18n from "I18n"; @@ -78,11 +79,10 @@ module( ); }); - test("has a dismiss button if some notifications are not read", async function (assert) { - notificationsData.forEach((notification) => { - notification.read = true; + test("has a dismiss button if some notification types have unread notifications", async function (assert) { + this.currentUser.set("grouped_unread_notifications", { + [NOTIFICATION_TYPES.mentioned]: 1, }); - notificationsData[0].read = false; await render(template); const dismissButton = query( ".panel-body-bottom .btn.notifications-dismiss" @@ -109,9 +109,8 @@ module( test("dismiss button makes a request to the server and then refreshes the notifications list", async function (assert) { await render(template); - notificationsData = getNotificationsData(); - notificationsData.forEach((notification) => { - notification.read = true; + this.currentUser.set("grouped_unread_notifications", { + [NOTIFICATION_TYPES.mentioned]: 1, }); assert.strictEqual(notificationsFetches, 1); await click(".panel-body-bottom .btn.notifications-dismiss"); @@ -126,5 +125,114 @@ module( "dismiss button is not shown" ); }); + + test("all notifications tab shows pending reviewables and sorts them with unread notifications based on their creation date", async function (assert) { + pretender.get("/notifications", () => { + return response({ + notifications: [ + { + id: 6, + user_id: 1, + notification_type: NOTIFICATION_TYPES.mentioned, + read: false, + high_priority: false, + created_at: "2021-11-25T19:31:13.241Z", + post_number: 6, + topic_id: 10, + fancy_title: "Unread notification #01", + slug: "unread-notification-01", + data: { + topic_title: "Unread notification #01", + original_post_id: 20, + original_post_type: 1, + original_username: "discobot", + revision_number: null, + display_username: "discobot", + }, + }, + { + id: 13, + user_id: 1, + notification_type: NOTIFICATION_TYPES.replied, + read: false, + high_priority: false, + created_at: "2021-08-25T19:31:13.241Z", + post_number: 6, + topic_id: 10, + fancy_title: "Unread notification #02", + slug: "unread-notification-02", + data: { + topic_title: "Unread notification #02", + original_post_id: 20, + original_post_type: 1, + original_username: "discobot", + revision_number: null, + display_username: "discobot", + }, + }, + { + id: 81, + user_id: 1, + notification_type: NOTIFICATION_TYPES.mentioned, + read: true, + high_priority: false, + created_at: "2022-10-25T19:31:13.241Z", + post_number: 6, + topic_id: 10, + fancy_title: "Read notification #01", + slug: "read-notification-01", + data: { + topic_title: "Read notification #01", + original_post_id: 20, + original_post_type: 1, + original_username: "discobot", + revision_number: null, + display_username: "discobot", + }, + }, + ], + pending_reviewables: [ + { + flagger_username: "sayo2", + id: 83, + pending: true, + topic_fancy_title: "anything hello world 0011", + type: "ReviewableQueuedPost", + created_at: "2022-09-25T19:31:13.241Z", + }, + { + flagger_username: "sayo2", + id: 78, + pending: true, + topic_fancy_title: "anything hello world 0033", + type: "ReviewableQueuedPost", + created_at: "2021-06-25T19:31:13.241Z", + }, + ], + }); + }); + await render(template); + const items = queryAll("ul li"); + assert.ok( + items[0].textContent.includes("hello world 0011"), + "the first pending reviewable is displayed 1st because it's most recent among pending reviewables and unread notifications" + ); + assert.ok( + items[1].textContent.includes("Unread notification #01"), + "the first unread notification is displayed 2nd because it's the 2nd most recent among pending reviewables and unread notifications" + ); + assert.ok( + items[2].textContent.includes("Unread notification #02"), + "the second unread notification is displayed 3rd because it's the 3rd most recent among pending reviewables and unread notifications" + ); + assert.ok( + items[3].textContent.includes("hello world 0033"), + "the second pending reviewable is displayed 4th because it's the 4th most recent among pending reviewables and unread notifications" + ); + assert.ok( + items[4].textContent.includes("Read notification #01"), + "read notifications come after the pending reviewables and unread notifications" + ); + }); } ); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js b/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js index ebc2f66d70..6bb987c852 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js @@ -12,6 +12,7 @@ import { getRawSize, inCodeBlock, initializeDefaultHomepage, + mergeSortedLists, setCaretPosition, setDefaultHomepage, slugify, @@ -288,6 +289,45 @@ discourseModule("Unit | Utilities", function () { } }); }); + + test("mergeSortedLists", function (assert) { + const comparator = (a, b) => b > a; + assert.deepEqual( + mergeSortedLists([], [1, 2, 3], comparator), + [1, 2, 3], + "it doesn't error when the first list is blank" + ); + assert.deepEqual( + mergeSortedLists([3, 2, 1], [], comparator), + [3, 2, 1], + "it doesn't error when the second list is blank" + ); + assert.deepEqual( + mergeSortedLists([], [], comparator), + [], + "it doesn't error when the both lists are blank" + ); + assert.deepEqual( + mergeSortedLists([5, 4, 0, -1], [1], comparator), + [5, 4, 1, 0, -1], + "it correctly merges lists when one list has 1 item only" + ); + assert.deepEqual( + mergeSortedLists([2], [1], comparator), + [2, 1], + "it correctly merges lists when both lists has 1 item each" + ); + assert.deepEqual( + mergeSortedLists([1], [1], comparator), + [1, 1], + "it correctly merges lists when both lists has 1 item and their items are identical" + ); + assert.deepEqual( + mergeSortedLists([5, 4, 3, 2, 1], [6, 2, 1], comparator), + [6, 5, 4, 3, 2, 2, 1, 1], + "it correctly merges lists that share common items" + ); + }); }); discourseModule("Unit | Utilities | clipboard", function (hooks) { diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index a33964575d..5566dd488e 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -30,8 +30,11 @@ class NotificationsController < ApplicationController limit = (params[:limit] || 15).to_i limit = 50 if limit > 50 + include_reviewables = false if SiteSetting.enable_experimental_sidebar_hamburger notifications = Notification.prioritized_list(current_user, count: limit, types: notification_types) + # notification_types is blank for the "all notifications" user menu tab + include_reviewables = notification_types.blank? && guardian.can_see_review_queue? else notifications = Notification.recent_report(current_user, limit, notification_types) end @@ -43,26 +46,28 @@ class NotificationsController < ApplicationController end end - if !params.has_key?(:silent) && params[:bump_last_seen_reviewable] && !@readonly_mode + if !params.has_key?(:silent) && params[:bump_last_seen_reviewable] && !@readonly_mode && include_reviewables current_user_id = current_user.id Scheduler::Defer.later "bump last seen reviewable for user" do # we lookup current_user again in the background thread to avoid - # concurrency issues where the objects returned by the current_user - # and/or methods are changed by the time the deferred block is - # executed - user = User.find_by(id: current_user_id) - next if user.blank? - new_guardian = Guardian.new(user) - if new_guardian.can_see_review_queue? - user.bump_last_seen_reviewable! - end + # concurrency issues where the user object returned by the + # current_user controller method is changed by the time the deferred + # block is executed + User.find_by(id: current_user_id)&.bump_last_seen_reviewable! end end - render_json_dump( + json = { notifications: serialize_data(notifications, NotificationSerializer), seen_notification_id: current_user.seen_notification_id - ) + } + if include_reviewables + json[:pending_reviewables] = Reviewable.basic_serializers_for_list( + Reviewable.user_menu_list_for(current_user), + current_user + ).as_json + end + render_json_dump(json) else offset = params[:offset].to_i diff --git a/app/controllers/reviewables_controller.rb b/app/controllers/reviewables_controller.rb index 06d87927e4..c6f0a35fae 100644 --- a/app/controllers/reviewables_controller.rb +++ b/app/controllers/reviewables_controller.rb @@ -71,9 +71,11 @@ class ReviewablesController < ApplicationController end def user_menu_list - reviewables = Reviewable.list_for(current_user, limit: 30, status: :pending).to_a json = { - reviewables: reviewables.map! { |r| r.basic_serializer.new(r, scope: guardian, root: nil).as_json } + reviewables: Reviewable.basic_serializers_for_list( + Reviewable.user_menu_list_for(current_user), + current_user + ).as_json } render_json_dump(json, rest_serializer: true) end diff --git a/app/models/reviewable.rb b/app/models/reviewable.rb index b8f57b2b1f..b955e77af0 100644 --- a/app/models/reviewable.rb +++ b/app/models/reviewable.rb @@ -534,6 +534,14 @@ class Reviewable < ActiveRecord::Base results end + def self.user_menu_list_for(user, limit: 30) + list_for(user, limit: limit, status: :pending).to_a + end + + def self.basic_serializers_for_list(reviewables, user) + reviewables.map { |r| r.basic_serializer.new(r, scope: user.guardian, root: nil) } + end + def serializer self.class.serializer_for(self) end diff --git a/app/serializers/basic_reviewable_serializer.rb b/app/serializers/basic_reviewable_serializer.rb index 35d678fd43..a2411c799f 100644 --- a/app/serializers/basic_reviewable_serializer.rb +++ b/app/serializers/basic_reviewable_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class BasicReviewableSerializer < ApplicationSerializer - attributes :flagger_username, :id, :type, :pending + attributes :flagger_username, :id, :type, :pending, :created_at def flagger_username object.created_by&.username diff --git a/spec/requests/notifications_controller_spec.rb b/spec/requests/notifications_controller_spec.rb index 2bade5ac1e..f263fef37d 100644 --- a/spec/requests/notifications_controller_spec.rb +++ b/spec/requests/notifications_controller_spec.rb @@ -87,60 +87,6 @@ RSpec.describe NotificationsController do Discourse.clear_redis_readonly! end - it "should not bump last seen reviewable in readonly mode" do - user.update!(admin: true) - Fabricate(:reviewable) - Discourse.received_redis_readonly! - expect { - get "/notifications.json", params: { recent: true } - expect(response.status).to eq(200) - }.not_to change { user.reload.last_seen_reviewable_id } - ensure - Discourse.clear_redis_readonly! - end - - it "should not bump last seen reviewable if the user can't seen reviewables" do - Fabricate(:reviewable) - expect { - get "/notifications.json", params: { recent: true, bump_last_seen_reviewable: true } - expect(response.status).to eq(200) - }.not_to change { user.reload.last_seen_reviewable_id } - end - - it "should not bump last seen reviewable if the silent param is present" do - user.update!(admin: true) - Fabricate(:reviewable) - expect { - get "/notifications.json", params: { - recent: true, - silent: true, - bump_last_seen_reviewable: true - } - expect(response.status).to eq(200) - }.not_to change { user.reload.last_seen_reviewable_id } - end - - it "should not bump last seen reviewable if the bump_last_seen_reviewable param is not present" do - user.update!(admin: true) - Fabricate(:reviewable) - expect { - get "/notifications.json", params: { recent: true, silent: true } - expect(response.status).to eq(200) - }.not_to change { user.reload.last_seen_reviewable_id } - end - - it "bumps last_seen_reviewable_id" do - user.update!(admin: true) - expect(user.last_seen_reviewable_id).to eq(nil) - reviewable = Fabricate(:reviewable) - get "/notifications.json", params: { recent: true, bump_last_seen_reviewable: true } - expect(user.reload.last_seen_reviewable_id).to eq(reviewable.id) - - reviewable2 = Fabricate(:reviewable) - get "/notifications.json", params: { recent: true, bump_last_seen_reviewable: true } - expect(user.reload.last_seen_reviewable_id).to eq(reviewable2.id) - end - it "get notifications with all filters" do notification = Fabricate(:notification, user: user) notification2 = Fabricate(:notification, user: user) @@ -202,6 +148,7 @@ RSpec.describe NotificationsController do created_at: 4.minutes.ago ) end + fab!(:pending_reviewable) { Fabricate(:reviewable) } it "gets notifications list with unread ones at the top when the setting is enabled" do SiteSetting.enable_experimental_sidebar_hamburger = true @@ -228,6 +175,93 @@ RSpec.describe NotificationsController do read_high_priority.id ]) end + + it "should not bump last seen reviewable in readonly mode" do + SiteSetting.enable_experimental_sidebar_hamburger = true + user.update!(admin: true) + Discourse.received_redis_readonly! + expect { + get "/notifications.json", params: { recent: true, bump_last_seen_reviewable: true } + expect(response.status).to eq(200) + }.not_to change { user.reload.last_seen_reviewable_id } + ensure + Discourse.clear_redis_readonly! + end + + it "should not bump last seen reviewable if the user can't see reviewables" do + SiteSetting.enable_experimental_sidebar_hamburger = true + expect { + get "/notifications.json", params: { recent: true, bump_last_seen_reviewable: true } + expect(response.status).to eq(200) + }.not_to change { user.reload.last_seen_reviewable_id } + end + + it "should not bump last seen reviewable if the silent param is present" do + SiteSetting.enable_experimental_sidebar_hamburger = true + user.update!(admin: true) + expect { + get "/notifications.json", params: { + recent: true, + silent: true, + bump_last_seen_reviewable: true + } + expect(response.status).to eq(200) + }.not_to change { user.reload.last_seen_reviewable_id } + end + + it "should not bump last seen reviewable if the bump_last_seen_reviewable param is not present" do + SiteSetting.enable_experimental_sidebar_hamburger = true + user.update!(admin: true) + expect { + get "/notifications.json", params: { recent: true } + expect(response.status).to eq(200) + }.not_to change { user.reload.last_seen_reviewable_id } + end + + it "bumps last_seen_reviewable_id" do + SiteSetting.enable_experimental_sidebar_hamburger = true + user.update!(admin: true) + expect(user.last_seen_reviewable_id).to eq(nil) + get "/notifications.json", params: { recent: true, bump_last_seen_reviewable: true } + expect(user.reload.last_seen_reviewable_id).to eq(pending_reviewable.id) + + reviewable2 = Fabricate(:reviewable) + get "/notifications.json", params: { recent: true, bump_last_seen_reviewable: true } + expect(user.reload.last_seen_reviewable_id).to eq(reviewable2.id) + end + + it "includes pending reviewables when the setting is enabled" do + SiteSetting.enable_experimental_sidebar_hamburger = true + user.update!(admin: true) + pending_reviewable2 = Fabricate(:reviewable, created_at: 4.minutes.ago) + Fabricate(:reviewable, status: Reviewable.statuses[:approved]) + Fabricate(:reviewable, status: Reviewable.statuses[:rejected]) + + get "/notifications.json", params: { recent: true } + expect(response.status).to eq(200) + expect(response.parsed_body["pending_reviewables"].map { |r| r["id"] }).to eq([ + pending_reviewable.id, + pending_reviewable2.id + ]) + end + + it "doesn't include reviewables when the setting is disabled" do + SiteSetting.enable_experimental_sidebar_hamburger = false + user.update!(admin: true) + + get "/notifications.json", params: { recent: true } + expect(response.status).to eq(200) + expect(response.parsed_body.key?("pending_reviewables")).to eq(false) + end + + it "doesn't include reviewables if the user can't see the review queue" do + SiteSetting.enable_experimental_sidebar_hamburger = true + user.update!(admin: false) + + get "/notifications.json", params: { recent: true } + expect(response.status).to eq(200) + expect(response.parsed_body.key?("pending_reviewables")).to eq(false) + end end context "when filter_by_types param is present" do diff --git a/spec/support/common_basic_reviewable_serializer.rb b/spec/support/common_basic_reviewable_serializer.rb index c439ddb15b..76680076c4 100644 --- a/spec/support/common_basic_reviewable_serializer.rb +++ b/spec/support/common_basic_reviewable_serializer.rb @@ -38,4 +38,12 @@ shared_examples "basic reviewable attributes" do expect(subject[:flagger_username]).to eq("gg.osama") end end + + describe "#created_at" do + it "serializes the reviewable's created_at field correctly" do + time = 10.minutes.ago + reviewable.update!(created_at: time) + expect(subject[:created_at]).to eq(time) + end + end end From 2559c763ad3ccff417b2673d6bb4fe9432565bba Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Wed, 5 Oct 2022 17:59:50 +0800 Subject: [PATCH 052/332] DEV: Fix message section link filters displaying icons (#18484) Follow-up to c0037dc0f06f08bd050aedc8aad97d1f05b322ab --- .../sidebar/user/messages-section/message-section-link.js | 8 ++++++-- .../acceptance/sidebar-user-messages-section-test.js | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/messages-section/message-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/messages-section/message-section-link.js index d1a10fefb0..a28cebdeb7 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/messages-section/message-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/messages-section/message-section-link.js @@ -73,10 +73,14 @@ export default class MessageSectionLink { } get prefixType() { - return "icon"; + if (this._isInbox) { + return "icon"; + } } get prefixValue() { - return "inbox"; + if (this._isInbox) { + return "inbox"; + } } } diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js index c42ba0d83e..f3ac3f2f4b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js @@ -161,6 +161,13 @@ acceptance( ), `personal message ${type} link is marked as active` ); + + assert.notOk( + exists( + `.sidebar-section-messages .sidebar-section-link-personal-messages-${type} .sidebar-section-link-prefix` + ), + `prefix is not displayed for ${type} personal message section link` + ); }); }); From 03b7b7d1bc7c978c280e441a610a5ef5c049c60c Mon Sep 17 00:00:00 2001 From: Dan Gebhardt Date: Wed, 5 Oct 2022 08:08:54 -0400 Subject: [PATCH 053/332] DEV: Remove usage of {{action}} modifiers - Take 2 (#18476) This PR enables the [`no-action-modifiers`](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-action-modifiers.md) template lint rule and removes all usages of the `{{action}}` modifier in core. In general, instances of `{{action "x"}}` have been replaced with `{{on "click" (action "x")}}`. In many cases, such as for `a` elements, we also need to prevent default event handling to avoid unwanted side effects. While the `{{action}}` modifier internally calls `event.preventDefault()`, we need to handle these cases more explicitly. For this purpose, this PR also adds the [ember-event-helpers](https://github.com/buschtoens/ember-event-helpers) dependency so we can use the `prevent-default` handler. For instance: ``` Do X ``` Note that `action` has not in general been refactored away as a helper yet. In general, all event handlers should be methods on the corresponding component and referenced directly (e.g. `{{on "click" this.doSomething}}`). However, the `action` helper is used extensively throughout the codebase and often references methods in the `actions` hash on controllers or routes. Thus this refactor will also be extensive and probably deserves a separate PR. Note: This work was done to complement #17767 by minimizing the potential impact of the `action` modifier override, which uses private API and arguably should be replaced with an AST transform. This is a followup to #18333, which had to be reverted because it did not account for the default treatment of modifier keys by the {{action}} modifier. Commits: * Enable `no-action-modifiers` template lint rule * Replace {{action "x"}} with {{on "click" (action "x")}} * Remove unnecessary action helper usage * Remove ctl+click tests for user-menu These tests now break in Chrome when used with addEventListener. As per the comment, they can probably be safely removed. * Prevent default event handlers to avoid unwanted side effects Uses `event.preventDefault()` in event handlers to prevent default event handling. This had been done automatically by the `action` modifier, but is not always desirable or necessary. * Restore UserCardContents#showUser action to avoid regression By keeping the `showUser` action, we can avoid a breaking change for plugins that rely upon it, while not interfering with the `showUser` argument that's been passed. * Revert EditCategoryTab#selectTab -> EditCategoryTab#select Avoid potential breaking change in themes / plugins * Restore GroupCardContents#showGroup action to avoid regression By keeping the `showGroup` action, we can avoid a breaking change for plugins that rely upon it, while not interfering with the `showGroup` argument that's been passed. * Restore SecondFactorAddTotp#showSecondFactorKey action to avoid regression By keeping the `showSecondFactorKey` action, we can avoid a breaking change for plugins that rely upon it, while not interfering with the `showSecondFactorKey` property that's maintained on the controller. * Refactor away from `actions` hash in ChooseMessage component * Modernize EmojiPicker#onCategorySelection usage * Modernize SearchResultEntry#logClick usage * Modernize Discovery::Categories#showInserted usage * Modernize Preferences::Account#resendConfirmationEmail usage * Modernize MultiSelect::SelectedCategory#onSelectedNameClick usage * Favor fn over action in SelectedChoice component * Modernize WizardStep event handlers * Favor fn over action usage in buttons * Restore Login#forgotPassword action to avoid possible regression * Introduce modKeysPressed utility Returns an array of modifier keys that are pressed during a given `MouseEvent` or `KeyboardEvent`. * Don't interfere with click events on links with `href` values when modifier keys are pressed --- .template-lintrc.js | 1 + .../addon/components/admin-editable-field.js | 14 +- .../addon/components/admin-theme-editor.js | 33 +++-- .../admin/addon/components/ip-lookup.js | 12 +- .../addon/components/themes-list-item.js | 9 +- .../addon/controllers/admin-badges/show.js | 6 + .../addon/controllers/admin-email-bounced.js | 7 + .../controllers/admin-email-preview-digest.js | 12 +- .../addon/controllers/admin-email-rejected.js | 7 + .../admin-logs-screened-ip-addresses.js | 17 ++- .../admin-logs-staff-action-logs.js | 125 ++++++++++-------- .../admin-web-hooks-show-events.js | 33 ++--- .../modals/admin-uploaded-image-list.js | 11 +- .../addon/templates/admin-badges/show.hbs | 4 +- .../components/admin-editable-field.hbs | 4 +- .../components/admin-theme-editor.hbs | 6 +- .../addon/templates/components/ip-lookup.hbs | 2 +- .../templates/components/themes-list-item.hbs | 2 +- .../admin/addon/templates/email-bounced.hbs | 4 +- .../addon/templates/email-preview-digest.hbs | 4 +- .../admin/addon/templates/email-rejected.hbs | 4 +- .../templates/logs/screened-ip-addresses.hbs | 2 +- .../templates/logs/staff-action-logs.hbs | 20 +-- .../modal/admin-uploaded-image-list.hbs | 2 +- .../addon/templates/web-hooks-show-events.hbs | 2 +- .../app/components/categories-only.js | 3 +- .../app/components/category-permission-row.js | 11 +- .../app/components/choose-message.js | 15 +-- .../app/components/composer-messages.js | 12 +- .../app/components/edit-category-tab.js | 16 +-- .../discourse/app/components/emoji-picker.js | 3 +- .../app/components/group-card-contents.js | 27 +++- .../components/group-imap-email-settings.js | 3 +- .../discourse/app/components/group-member.js | 10 +- .../components/group-smtp-email-settings.js | 3 +- .../discourse/app/components/mobile-nav.js | 41 +++--- .../app/components/navigation-bar.js | 43 +++--- .../app/components/reviewable-item.js | 47 ++++--- .../app/components/reviewable-post-edits.js | 25 ++-- .../app/components/reviewable-queued-post.js | 9 +- .../app/components/search-result-entry.js | 7 +- .../app/components/second-factor-form.js | 21 +-- .../app/components/security-key-form.js | 13 +- .../discourse/app/components/signup-cta.js | 12 +- .../discourse/app/components/tag-info.js | 71 +++++----- .../app/components/topic-list-item.js | 2 + .../app/components/user-card-contents.js | 21 ++- .../discourse/app/controllers/auth-token.js | 11 +- .../app/controllers/avatar-selector.js | 17 ++- .../discourse/app/controllers/composer.js | 48 ++++--- .../app/controllers/discovery/categories.js | 19 +-- .../app/controllers/discovery/topics.js | 21 +-- .../app/controllers/full-page-search.js | 31 +++-- .../discourse/app/controllers/history.js | 29 ++-- .../discourse/app/controllers/login.js | 111 +++++++++------- .../app/controllers/password-reset.js | 17 ++- .../app/controllers/preferences/account.js | 28 ++-- .../controllers/preferences/second-factor.js | 40 +++--- .../app/controllers/preferences/security.js | 98 +++++++------- .../app/controllers/second-factor-add-totp.js | 9 +- .../app/controllers/second-factor-auth.js | 3 +- .../discourse/app/controllers/tag-show.js | 3 +- .../discourse/app/controllers/tags-index.js | 37 +++--- .../discourse/app/controllers/topic.js | 92 +++++++------ .../controllers/user-private-messages-tags.js | 34 ++--- .../app/controllers/user-topics-list.js | 4 +- .../discourse/app/controllers/user.js | 16 ++- .../discourse/app/lib/utilities.js | 6 + .../templates/components/categories-only.hbs | 2 +- .../components/category-permission-row.hbs | 2 +- .../templates/components/choose-message.hbs | 2 +- .../components/edit-category-tab.hbs | 2 +- .../components/emoji-group-buttons.hbs | 18 +-- .../app/templates/components/emoji-picker.hbs | 6 +- .../templates/components/flag-action-type.hbs | 4 +- .../components/group-card-contents.hbs | 8 +- .../components/group-imap-email-settings.hbs | 2 +- .../group-manage-email-settings.hbs | 4 +- .../app/templates/components/group-member.hbs | 2 +- .../components/group-smtp-email-settings.hbs | 2 +- .../templates/components/login-buttons.hbs | 2 +- .../templates/components/reviewable-item.hbs | 2 +- .../components/reviewable-post-edits.hbs | 2 +- .../components/reviewable-queued-post.hbs | 2 +- .../components/search-result-entry.hbs | 2 +- .../components/second-factor-form.hbs | 2 +- .../components/security-key-form.hbs | 2 +- .../templates/components/selected-posts.hbs | 6 +- .../app/templates/components/signup-cta.hbs | 2 +- .../app/templates/components/tag-info.hbs | 6 +- .../components/user-card-contents.hbs | 6 +- .../discourse/app/templates/composer.hbs | 10 +- .../templates/composer/dominating-topic.hbs | 2 +- .../app/templates/composer/education.hbs | 2 +- .../app/templates/composer/get-a-room.hbs | 2 +- .../templates/composer/group-mentioned.hbs | 2 +- .../app/templates/composer/similar-topics.hbs | 2 +- .../app/templates/discovery/categories.hbs | 2 +- .../app/templates/discovery/topics.hbs | 2 +- .../app/templates/full-page-search.hbs | 2 +- .../mobile/components/categories-only.hbs | 2 +- .../mobile/components/mobile-nav.hbs | 2 +- .../mobile/components/navigation-bar.hbs | 2 +- .../app/templates/mobile/discovery/topics.hbs | 2 +- .../app/templates/mobile/modal/login.hbs | 4 +- .../app/templates/modal/auth-token.hbs | 2 +- .../app/templates/modal/avatar-selector.hbs | 2 +- .../discourse/app/templates/modal/history.hbs | 6 +- .../app/templates/modal/insert-hyperlink.hbs | 2 +- .../discourse/app/templates/modal/login.hbs | 4 +- .../modal/second-factor-add-totp.hbs | 2 +- .../app/templates/password-reset.hbs | 2 +- .../templates/preferences-second-factor.hbs | 2 +- .../app/templates/preferences/account.hbs | 2 +- .../app/templates/preferences/security.hbs | 6 +- .../app/templates/second-factor-auth.hbs | 2 +- .../discourse/app/templates/tag/show.hbs | 2 +- .../discourse/app/templates/tags/index.hbs | 4 +- .../discourse/app/templates/topic.hbs | 6 +- .../templates/user-private-messages-tags.hbs | 4 +- .../app/templates/user-topics-list.hbs | 2 +- .../discourse/app/templates/user.hbs | 2 +- .../plugin-outlet-connector-class-test.js | 4 +- .../tests/acceptance/user-menu-test.js | 19 --- .../fixtures/concerns/notification-types.js | 1 + .../tests/unit/lib/utilities-test.js | 52 ++++++++ .../multi-select/selected-category.hbs | 2 +- .../templates/components/selected-choice.hbs | 2 +- .../addon/components/styling-preview.js | 6 +- .../wizard/addon/components/wizard-step.js | 14 +- .../templates/components/styling-preview.hbs | 4 +- .../templates/components/wizard-step.hbs | 10 +- lib/tasks/javascript.rake | 2 +- .../discourse-local-dates-create-form.js | 8 +- .../discourse-local-dates-create-form.hbs | 2 +- .../controllers/poll-ui-builder.js | 6 + .../templates/modal/poll-ui-builder.hbs | 6 +- 137 files changed, 1013 insertions(+), 735 deletions(-) diff --git a/.template-lintrc.js b/.template-lintrc.js index e3fc07b48d..b5095385b2 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -3,6 +3,7 @@ module.exports = { extends: "discourse:recommended", rules: { + "no-action-modifiers": true, "no-capital-arguments": false, // TODO: we extensively use `args` argument name "no-curly-component-invocation": { allow: [ diff --git a/app/assets/javascripts/admin/addon/components/admin-editable-field.js b/app/assets/javascripts/admin/addon/components/admin-editable-field.js index 993a3ed675..892a3208e8 100644 --- a/app/assets/javascripts/admin/addon/components/admin-editable-field.js +++ b/app/assets/javascripts/admin/addon/components/admin-editable-field.js @@ -1,4 +1,6 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; + export default Component.extend({ tagName: "", @@ -10,12 +12,14 @@ export default Component.extend({ this.set("editing", false); }, - actions: { - edit() { - this.set("buffer", this.value); - this.toggleProperty("editing"); - }, + @action + edit(event) { + event?.preventDefault(); + this.set("buffer", this.value); + this.toggleProperty("editing"); + }, + actions: { save() { // Action has to toggle 'editing' property. this.action(this.buffer); diff --git a/app/assets/javascripts/admin/addon/components/admin-theme-editor.js b/app/assets/javascripts/admin/addon/components/admin-theme-editor.js index 014055c149..de8f063579 100644 --- a/app/assets/javascripts/admin/addon/components/admin-theme-editor.js +++ b/app/assets/javascripts/admin/addon/components/admin-theme-editor.js @@ -3,6 +3,7 @@ import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { fmt } from "discourse/lib/computed"; import { isDocumentRTL } from "discourse/lib/text-direction"; +import { action } from "@ember/object"; import { next } from "@ember/runloop"; export default Component.extend({ @@ -91,15 +92,26 @@ export default Component.extend({ return this.theme.getError(target, fieldName); }, + @action + toggleShowAdvanced(event) { + event?.preventDefault(); + this.toggleProperty("showAdvanced"); + }, + + @action + toggleAddField(event) { + event?.preventDefault(); + this.toggleProperty("addingField"); + }, + + @action + toggleMaximize(event) { + event?.preventDefault(); + this.toggleProperty("maximized"); + next(() => this.appEvents.trigger("ace:resize")); + }, + actions: { - toggleShowAdvanced() { - this.toggleProperty("showAdvanced"); - }, - - toggleAddField() { - this.toggleProperty("addingField"); - }, - cancelAddField() { this.set("addingField", false); }, @@ -114,11 +126,6 @@ export default Component.extend({ this.fieldAdded(this.currentTargetName, name); }, - toggleMaximize() { - this.toggleProperty("maximized"); - next(() => this.appEvents.trigger("ace:resize")); - }, - onlyOverriddenChanged(value) { this.onlyOverriddenChanged(value); }, diff --git a/app/assets/javascripts/admin/addon/components/ip-lookup.js b/app/assets/javascripts/admin/addon/components/ip-lookup.js index 222e04b9ab..02a5240564 100644 --- a/app/assets/javascripts/admin/addon/components/ip-lookup.js +++ b/app/assets/javascripts/admin/addon/components/ip-lookup.js @@ -1,6 +1,6 @@ import AdminUser from "admin/models/admin-user"; import Component from "@ember/component"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import { ajax } from "discourse/lib/ajax"; import copyText from "discourse/lib/copy-text"; @@ -21,6 +21,12 @@ export default Component.extend({ return Math.max(visible, total); }, + @action + hide(event) { + event?.preventDefault(); + this.set("show", false); + }, + actions: { lookup() { this.set("show", true); @@ -55,10 +61,6 @@ export default Component.extend({ } }, - hide() { - this.set("show", false); - }, - copy() { let text = `IP: ${this.ip}\n`; const location = this.location; diff --git a/app/assets/javascripts/admin/addon/components/themes-list-item.js b/app/assets/javascripts/admin/addon/components/themes-list-item.js index e2237e249b..29158c4496 100644 --- a/app/assets/javascripts/admin/addon/components/themes-list-item.js +++ b/app/assets/javascripts/admin/addon/components/themes-list-item.js @@ -3,6 +3,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import Component from "@ember/component"; import { escape } from "pretty-text/sanitizer"; import { iconHTML } from "discourse-common/lib/icon-library"; +import { action } from "@ember/object"; const MAX_COMPONENTS = 4; @@ -59,9 +60,9 @@ export default Component.extend({ return childrenCount - MAX_COMPONENTS; }, - actions: { - toggleChildrenExpanded() { - this.toggleProperty("childrenExpanded"); - }, + @action + toggleChildrenExpanded(event) { + event?.preventDefault(); + this.toggleProperty("childrenExpanded"); }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-badges/show.js b/app/assets/javascripts/admin/addon/controllers/admin-badges/show.js index 2b38081826..5b098387ef 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-badges/show.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-badges/show.js @@ -133,6 +133,12 @@ export default class AdminBadgesShowController extends Controller.extend( this.buffered.set("image_url", null); } + @action + showPreview(badge, explain, event) { + event?.preventDefault(); + this.send("preview", badge, explain); + } + @action save() { if (!this.saving) { diff --git a/app/assets/javascripts/admin/addon/controllers/admin-email-bounced.js b/app/assets/javascripts/admin/addon/controllers/admin-email-bounced.js index 1500549564..c7a8ec98a6 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-email-bounced.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-email-bounced.js @@ -2,8 +2,15 @@ import AdminEmailLogsController from "admin/controllers/admin-email-logs"; import { INPUT_DELAY } from "discourse-common/config/environment"; import discourseDebounce from "discourse-common/lib/debounce"; import { observes } from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; export default AdminEmailLogsController.extend({ + @action + handleShowIncomingEmail(id, event) { + event?.preventDefault(); + this.send("showIncomingEmail", id); + }, + @observes("filter.{status,user,address,type}") filterEmailLogs() { discourseDebounce(this, this.loadLogs, INPUT_DELAY); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js b/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js index 50d74d35d8..84f85eea6d 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js @@ -1,7 +1,7 @@ import { empty, notEmpty, or } from "@ember/object/computed"; import Controller from "@ember/controller"; import EmailPreview from "admin/models/email-preview"; -import { get } from "@ember/object"; +import { action, get } from "@ember/object"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { inject as service } from "@ember/service"; @@ -14,6 +14,12 @@ export default Controller.extend({ showSendEmailForm: notEmpty("model.html_content"), htmlEmpty: empty("model.html_content"), + @action + toggleShowHtml(event) { + event?.preventDefault(); + this.toggleProperty("showHtml"); + }, + actions: { updateUsername(selected) { this.set("username", get(selected, "firstObject")); @@ -39,10 +45,6 @@ export default Controller.extend({ }); }, - toggleShowHtml() { - this.toggleProperty("showHtml"); - }, - sendEmail() { this.set("sendingEmail", true); this.set("sentEmail", false); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-email-rejected.js b/app/assets/javascripts/admin/addon/controllers/admin-email-rejected.js index 89c67f3cf9..6e4ce78656 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-email-rejected.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-email-rejected.js @@ -3,6 +3,7 @@ import { INPUT_DELAY } from "discourse-common/config/environment"; import IncomingEmail from "admin/models/incoming-email"; import discourseDebounce from "discourse-common/lib/debounce"; import { observes } from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; export default AdminEmailLogsController.extend({ @observes("filter.{status,from,to,subject,error}") @@ -10,6 +11,12 @@ export default AdminEmailLogsController.extend({ discourseDebounce(this, this.loadLogs, IncomingEmail, INPUT_DELAY); }, + @action + handleShowIncomingEmail(id, event) { + event?.preventDefault(); + this.send("showIncomingEmail", id); + }, + actions: { loadMore() { this.loadLogs(IncomingEmail, true); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-logs-screened-ip-addresses.js b/app/assets/javascripts/admin/addon/controllers/admin-logs-screened-ip-addresses.js index 0e8b43f9c1..bc11ec51f4 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-logs-screened-ip-addresses.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-logs-screened-ip-addresses.js @@ -6,6 +6,7 @@ import discourseDebounce from "discourse-common/lib/debounce"; import { exportEntity } from "discourse/lib/export-csv"; import { observes } from "discourse-common/utils/decorators"; import { outputExportResult } from "discourse/lib/export-result"; +import { action } from "@ember/object"; import { inject as service } from "@ember/service"; export default Controller.extend({ @@ -26,6 +27,15 @@ export default Controller.extend({ discourseDebounce(this, this._debouncedShow, INPUT_DELAY); }, + @action + edit(record, event) { + event?.preventDefault(); + if (!record.get("editing")) { + this.set("savedIpAddress", record.get("ip_address")); + } + record.set("editing", true); + }, + actions: { allow(record) { record.set("action_name", "do_nothing"); @@ -37,13 +47,6 @@ export default Controller.extend({ record.save(); }, - edit(record) { - if (!record.get("editing")) { - this.set("savedIpAddress", record.get("ip_address")); - } - record.set("editing", true); - }, - cancel(record) { const savedIpAddress = this.savedIpAddress; if (savedIpAddress && record.get("editing")) { diff --git a/app/assets/javascripts/admin/addon/controllers/admin-logs-staff-action-logs.js b/app/assets/javascripts/admin/addon/controllers/admin-logs-staff-action-logs.js index 670d48c17b..a8a297d47c 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-logs-staff-action-logs.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-logs-staff-action-logs.js @@ -1,5 +1,5 @@ import Controller from "@ember/controller"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { exportEntity } from "discourse/lib/export-csv"; @@ -31,11 +31,13 @@ export default Controller.extend({ this.set( "userHistoryActions", result.extras.user_history_actions - .map((action) => ({ - id: action.id, - action_id: action.action_id, - name: I18n.t("admin.logs.staff_actions.actions." + action.id), - name_raw: action.id, + .map((historyAction) => ({ + id: historyAction.id, + action_id: historyAction.action_id, + name: I18n.t( + "admin.logs.staff_actions.actions." + historyAction.id + ), + name_raw: historyAction.id, })) .sort((a, b) => a.name.localeCompare(b.name)) ); @@ -75,61 +77,74 @@ export default Controller.extend({ this.scheduleRefresh(); }, - actions: { - filterActionIdChanged(filterActionId) { - if (filterActionId) { - this.changeFilters({ - action_name: filterActionId, - action_id: this.userHistoryActions.findBy("id", filterActionId) - .action_id, - }); - } - }, - - clearFilter(key) { - if (key === "actionFilter") { - this.set("filterActionId", null); - this.changeFilters({ - action_name: null, - action_id: null, - custom_type: null, - }); - } else { - this.changeFilters({ [key]: null }); - } - }, - - clearAllFilters() { - this.set("filterActionId", null); - this.resetFilters(); - }, - - filterByAction(logItem) { + @action + filterActionIdChanged(filterActionId) { + if (filterActionId) { this.changeFilters({ - action_name: logItem.get("action_name"), - action_id: logItem.get("action"), - custom_type: logItem.get("custom_type"), + action_name: filterActionId, + action_id: this.userHistoryActions.findBy("id", filterActionId) + .action_id, }); - }, + } + }, - filterByStaffUser(acting_user) { - this.changeFilters({ acting_user: acting_user.username }); - }, + @action + clearFilter(key, event) { + event?.preventDefault(); + if (key === "actionFilter") { + this.set("filterActionId", null); + this.changeFilters({ + action_name: null, + action_id: null, + custom_type: null, + }); + } else { + this.changeFilters({ [key]: null }); + } + }, - filterByTargetUser(target_user) { - this.changeFilters({ target_user: target_user.username }); - }, + @action + clearAllFilters(event) { + event?.preventDefault(); + this.set("filterActionId", null); + this.resetFilters(); + }, - filterBySubject(subject) { - this.changeFilters({ subject }); - }, + @action + filterByAction(logItem, event) { + event?.preventDefault(); + this.changeFilters({ + action_name: logItem.get("action_name"), + action_id: logItem.get("action"), + custom_type: logItem.get("custom_type"), + }); + }, - exportStaffActionLogs() { - exportEntity("staff_action").then(outputExportResult); - }, + @action + filterByStaffUser(acting_user, event) { + event?.preventDefault(); + this.changeFilters({ acting_user: acting_user.username }); + }, - loadMore() { - this.model.loadMore(); - }, + @action + filterByTargetUser(target_user, event) { + event?.preventDefault(); + this.changeFilters({ target_user: target_user.username }); + }, + + @action + filterBySubject(subject, event) { + event?.preventDefault(); + this.changeFilters({ subject }); + }, + + @action + exportStaffActionLogs() { + exportEntity("staff_action").then(outputExportResult); + }, + + @action + loadMore() { + this.model.loadMore(); }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js index 71990f47a3..84b7650ff1 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js @@ -1,5 +1,6 @@ import Controller from "@ember/controller"; import { ajax } from "discourse/lib/ajax"; +import { action } from "@ember/object"; import { alias } from "@ember/object/computed"; import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; @@ -43,6 +44,23 @@ export default Controller.extend({ } }, + @action + showInserted(event) { + event?.preventDefault(); + const webHookId = this.get("model.extras.web_hook_id"); + + ajax(`/admin/api/web_hooks/${webHookId}/events/bulk`, { + type: "GET", + data: { ids: this.incomingEventIds }, + }).then((data) => { + const objects = data.map((webHookEvent) => + this.store.createRecord("web-hook-event", webHookEvent) + ); + this.model.unshiftObjects(objects); + this.set("incomingEventIds", []); + }); + }, + actions: { loadMore() { this.model.loadMore(); @@ -61,20 +79,5 @@ export default Controller.extend({ popupAjaxError(error); }); }, - - showInserted() { - const webHookId = this.get("model.extras.web_hook_id"); - - ajax(`/admin/api/web_hooks/${webHookId}/events/bulk`, { - type: "GET", - data: { ids: this.incomingEventIds }, - }).then((data) => { - const objects = data.map((event) => - this.store.createRecord("web-hook-event", event) - ); - this.model.unshiftObjects(objects); - this.set("incomingEventIds", []); - }); - }, }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-uploaded-image-list.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-uploaded-image-list.js index 7ab4cb6984..8a60037a5b 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-uploaded-image-list.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-uploaded-image-list.js @@ -1,5 +1,6 @@ import { observes, on } from "discourse-common/utils/decorators"; import Controller from "@ember/controller"; +import { action } from "@ember/object"; import ModalFunctionality from "discourse/mixins/modal-functionality"; export default Controller.extend(ModalFunctionality, { @@ -10,15 +11,17 @@ export default Controller.extend(ModalFunctionality, { this.set("images", value && value.length ? value.split("|") : []); }, + @action + remove(url, event) { + event?.preventDefault(); + this.images.removeObject(url); + }, + actions: { uploadDone({ url }) { this.images.addObject(url); }, - remove(url) { - this.images.removeObject(url); - }, - close() { this.save(this.images.join("|")); this.send("closeModal"); diff --git a/app/assets/javascripts/admin/addon/templates/admin-badges/show.hbs b/app/assets/javascripts/admin/addon/templates/admin-badges/show.hbs index 31b16af4cc..82492beecb 100644 --- a/app/assets/javascripts/admin/addon/templates/admin-badges/show.hbs +++ b/app/assets/javascripts/admin/addon/templates/admin-badges/show.hbs @@ -88,9 +88,9 @@ {{#if this.hasQuery}} - {{i18n "admin.badges.preview.link_text"}} + {{i18n "admin.badges.preview.link_text"}} | - {{i18n "admin.badges.preview.plan_text"}} + {{i18n "admin.badges.preview.plan_text"}} {{#if this.preview_loading}} {{i18n "loading"}} {{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs index d62cfdbd78..998174f20a 100644 --- a/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs @@ -3,7 +3,7 @@ {{#if this.editing}} {{else}} - + {{this.value}} {{/if}} @@ -11,7 +11,7 @@
    {{#if this.editing}} - {{i18n "cancel"}} + {{i18n "cancel"}} {{else}} {{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs index ec8ed70f30..a8c33e1da2 100644 --- a/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs @@ -13,7 +13,7 @@ {{#if this.allowAdvanced}}
  • - @@ -52,7 +52,7 @@ {{else}} - + {{d-icon "plus"}} {{/if}} @@ -61,7 +61,7 @@
  • - + {{d-icon this.maximizeIcon}}
  • diff --git a/app/assets/javascripts/admin/addon/templates/components/ip-lookup.hbs b/app/assets/javascripts/admin/addon/templates/components/ip-lookup.hbs index f63ab16730..31ec7f35a7 100644 --- a/app/assets/javascripts/admin/addon/templates/components/ip-lookup.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/ip-lookup.hbs @@ -3,7 +3,7 @@ {{/if}} {{#if this.show}}
    - {{d-icon "times"}} + {{d-icon "times"}} {{#if this.copied}} {{else}} diff --git a/app/assets/javascripts/admin/addon/templates/components/themes-list-item.hbs b/app/assets/javascripts/admin/addon/templates/components/themes-list-item.hbs index 1710a14454..21678c1a43 100644 --- a/app/assets/javascripts/admin/addon/templates/components/themes-list-item.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/themes-list-item.hbs @@ -31,7 +31,7 @@ {{html-safe this.childrenString}} {{#if this.displayHasMore}} - + {{#if this.childrenExpanded}} {{i18n "admin.customize.theme.collapse"}} {{else}} diff --git a/app/assets/javascripts/admin/addon/templates/email-bounced.hbs b/app/assets/javascripts/admin/addon/templates/email-bounced.hbs index cf6ceb81bc..6a47751d00 100644 --- a/app/assets/javascripts/admin/addon/templates/email-bounced.hbs +++ b/app/assets/javascripts/admin/addon/templates/email-bounced.hbs @@ -30,7 +30,7 @@ {{l.to_address}} {{#if l.has_bounce_key}} - + {{l.email_type}} {{else}} @@ -39,7 +39,7 @@ {{#if l.has_bounce_key}} - + {{d-icon "info-circle"}} {{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/email-preview-digest.hbs b/app/assets/javascripts/admin/addon/templates/email-preview-digest.hbs index d2ee4cad4d..b00332840d 100644 --- a/app/assets/javascripts/admin/addon/templates/email-preview-digest.hbs +++ b/app/assets/javascripts/admin/addon/templates/email-preview-digest.hbs @@ -15,11 +15,11 @@ {{#if this.showHtml}} {{i18n "admin.email.html"}} | - + {{i18n "admin.email.text"}} {{else}} - {{i18n "admin.email.html"}} | + {{i18n "admin.email.html"}} | {{i18n "admin.email.text"}} {{/if}}
    diff --git a/app/assets/javascripts/admin/addon/templates/email-rejected.hbs b/app/assets/javascripts/admin/addon/templates/email-rejected.hbs index 21dcbba21d..19033f5f6f 100644 --- a/app/assets/javascripts/admin/addon/templates/email-rejected.hbs +++ b/app/assets/javascripts/admin/addon/templates/email-rejected.hbs @@ -48,10 +48,10 @@ {{email.subject}} - {{email.error}} + {{email.error}} - + {{d-icon "info-circle"}} diff --git a/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs b/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs index 63489dcdee..ab20b2316b 100644 --- a/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs +++ b/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs @@ -27,7 +27,7 @@ {{#if item.editing}} {{else}} - + {{#if item.isRange}} {{item.ip_address}} {{else}} diff --git a/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs b/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs index bcee25b439..d2e46867c9 100644 --- a/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs +++ b/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs @@ -1,29 +1,29 @@
    {{#if this.filtersExists}} - {{item.actionName}} + {{item.actionName}}
    {{#if item.target_user}} {{avatar item.target_user imageSize="tiny"}} - {{item.target_user.username}} + {{item.target_user.username}} {{/if}} {{#if item.subject}} - {{item.subject}} + {{item.subject}} {{/if}}
    @@ -89,10 +89,10 @@ diff --git a/app/assets/javascripts/admin/addon/templates/modal/admin-uploaded-image-list.hbs b/app/assets/javascripts/admin/addon/templates/modal/admin-uploaded-image-list.hbs index a7aefa7371..aca6fef6f4 100644 --- a/app/assets/javascripts/admin/addon/templates/modal/admin-uploaded-image-list.hbs +++ b/app/assets/javascripts/admin/addon/templates/modal/admin-uploaded-image-list.hbs @@ -1,7 +1,7 @@
    {{#each this.images as |image|}} - + {{bound-avatar-template image "huge"}} {{else}} diff --git a/app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs b/app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs index 2b5c768def..c9791f0fc4 100644 --- a/app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs +++ b/app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs @@ -21,7 +21,7 @@
    {{#if this.hasIncoming}} - + {{/if}} diff --git a/app/assets/javascripts/discourse/app/components/categories-only.js b/app/assets/javascripts/discourse/app/components/categories-only.js index 002ddf82e3..d480f46455 100644 --- a/app/assets/javascripts/discourse/app/components/categories-only.js +++ b/app/assets/javascripts/discourse/app/components/categories-only.js @@ -50,7 +50,8 @@ export default Component.extend({ }, @action - toggleShowMuted() { + toggleShowMuted(event) { + event?.preventDefault(); this.toggleProperty("showMuted"); }, }); diff --git a/app/assets/javascripts/discourse/app/components/category-permission-row.js b/app/assets/javascripts/discourse/app/components/category-permission-row.js index 2e41aa6e63..46eb3690f9 100644 --- a/app/assets/javascripts/discourse/app/components/category-permission-row.js +++ b/app/assets/javascripts/discourse/app/components/category-permission-row.js @@ -1,3 +1,4 @@ +import { action } from "@ember/object"; import { alias, equal } from "@ember/object/computed"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; import Component from "@ember/component"; @@ -92,11 +93,13 @@ export default Component.extend({ this.category.updatePermission(this.group_name, type); }, - actions: { - removeRow() { - this.category.removePermission(this.group_name); - }, + @action + removeRow(event) { + event?.preventDefault(); + this.category.removePermission(this.group_name); + }, + actions: { setPermissionReply() { if (this.type <= PermissionType.CREATE_POST) { this.updatePermission(PermissionType.READONLY); diff --git a/app/assets/javascripts/discourse/app/components/choose-message.js b/app/assets/javascripts/discourse/app/components/choose-message.js index 09d9a9123c..15f0424f60 100644 --- a/app/assets/javascripts/discourse/app/components/choose-message.js +++ b/app/assets/javascripts/discourse/app/components/choose-message.js @@ -1,6 +1,6 @@ import Component from "@ember/component"; import discourseDebounce from "discourse-common/lib/debounce"; -import { get } from "@ember/object"; +import { action, get } from "@ember/object"; import { isEmpty } from "@ember/utils"; import { next } from "@ember/runloop"; import { observes } from "discourse-common/utils/decorators"; @@ -63,12 +63,11 @@ export default Component.extend({ ); }, - actions: { - chooseMessage(message) { - const messageId = get(message, "id"); - this.set("selectedTopicId", messageId); - next(() => $(`#choose-message-${messageId}`).prop("checked", "true")); - return false; - }, + @action + chooseMessage(message, event) { + event?.preventDefault(); + const messageId = get(message, "id"); + this.set("selectedTopicId", messageId); + next(() => $(`#choose-message-${messageId}`).prop("checked", "true")); }, }); diff --git a/app/assets/javascripts/discourse/app/components/composer-messages.js b/app/assets/javascripts/discourse/app/components/composer-messages.js index 40832b5df1..275d1c534c 100644 --- a/app/assets/javascripts/discourse/app/components/composer-messages.js +++ b/app/assets/javascripts/discourse/app/components/composer-messages.js @@ -1,5 +1,5 @@ import Component from "@ember/component"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import LinkLookup from "discourse/lib/link-lookup"; import { not } from "@ember/object/computed"; @@ -54,11 +54,13 @@ export default Component.extend({ this.set("messageCount", messages.get("length")); }, - actions: { - closeMessage(message) { - this._removeMessage(message); - }, + @action + closeMessage(message, event) { + event?.preventDefault(); + this._removeMessage(message); + }, + actions: { hideMessage(message) { this._removeMessage(message); // kind of hacky but the visibility depends on this diff --git a/app/assets/javascripts/discourse/app/components/edit-category-tab.js b/app/assets/javascripts/discourse/app/components/edit-category-tab.js index e96720db36..5619e8c753 100644 --- a/app/assets/javascripts/discourse/app/components/edit-category-tab.js +++ b/app/assets/javascripts/discourse/app/components/edit-category-tab.js @@ -2,6 +2,7 @@ import Component from "@ember/component"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; import { empty } from "@ember/object/computed"; import getURL from "discourse-common/lib/get-url"; import { propertyEqual } from "discourse/lib/computed"; @@ -49,13 +50,12 @@ export default Component.extend({ return getURL(`/c/${slugPart}/edit/${this.tab}`); }, - actions: { - select() { - this.set("selectedTab", this.tab); - - if (!this.newCategory) { - DiscourseURL.routeTo(this.fullSlug); - } - }, + @action + select(event) { + event?.preventDefault(); + this.set("selectedTab", this.tab); + if (!this.newCategory) { + DiscourseURL.routeTo(this.fullSlug); + } }, }); diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker.js b/app/assets/javascripts/discourse/app/components/emoji-picker.js index 3e0646f893..8a5dec83a6 100644 --- a/app/assets/javascripts/discourse/app/components/emoji-picker.js +++ b/app/assets/javascripts/discourse/app/components/emoji-picker.js @@ -240,7 +240,8 @@ export default Component.extend({ }, @action - onCategorySelection(sectionName) { + onCategorySelection(sectionName, event) { + event?.preventDefault(); const section = document.querySelector( `.emoji-picker-emoji-area .section[data-section="${sectionName}"]` ); diff --git a/app/assets/javascripts/discourse/app/components/group-card-contents.js b/app/assets/javascripts/discourse/app/components/group-card-contents.js index cb073a6733..547840d197 100644 --- a/app/assets/javascripts/discourse/app/components/group-card-contents.js +++ b/app/assets/javascripts/discourse/app/components/group-card-contents.js @@ -1,3 +1,4 @@ +import { action } from "@ember/object"; import { alias, gt } from "@ember/object/computed"; import CardContentsBase from "discourse/mixins/card-contents-base"; import CleansUp from "discourse/mixins/cleans-up"; @@ -6,6 +7,7 @@ import { Promise } from "rsvp"; import discourseComputed from "discourse-common/utils/decorators"; import { groupPath } from "discourse/lib/url"; import { setting } from "discourse/lib/computed"; +import { modKeysPressed } from "discourse/lib/utilities"; const maxMembersToDisplay = 10; @@ -70,11 +72,25 @@ export default Component.extend(CardContentsBase, CleansUp, { this._close(); }, - actions: { - close() { - this._close(); - }, + @action + close(event) { + event?.preventDefault(); + this._close(); + }, + @action + handleShowGroup(group, event) { + if (event && modKeysPressed(event).length > 0) { + return false; + } + event?.preventDefault(); + // Invokes `showGroup` argument. Convert to `this.args.showGroup` when + // refactoring this to a glimmer component. + this.showGroup(group); + this._close(); + }, + + actions: { cancelFilter() { const postStream = this.postStream; postStream.cancelFilter(); @@ -90,8 +106,7 @@ export default Component.extend(CardContentsBase, CleansUp, { }, showGroup(group) { - this.showGroup(group); - this._close(); + this.handleShowGroup(group); }, }, }); diff --git a/app/assets/javascripts/discourse/app/components/group-imap-email-settings.js b/app/assets/javascripts/discourse/app/components/group-imap-email-settings.js index 190a9fb2f9..a437f42d44 100644 --- a/app/assets/javascripts/discourse/app/components/group-imap-email-settings.js +++ b/app/assets/javascripts/discourse/app/components/group-imap-email-settings.js @@ -53,7 +53,8 @@ export default Component.extend({ }, @action - prefillSettings(provider) { + prefillSettings(provider, event) { + event?.preventDefault(); this.form.setProperties(emailProviderDefaultSettings(provider, "imap")); }, diff --git a/app/assets/javascripts/discourse/app/components/group-member.js b/app/assets/javascripts/discourse/app/components/group-member.js index 64b2570dee..08d1097b42 100644 --- a/app/assets/javascripts/discourse/app/components/group-member.js +++ b/app/assets/javascripts/discourse/app/components/group-member.js @@ -1,10 +1,12 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; + export default Component.extend({ classNames: ["item"], - actions: { - remove() { - this.removeAction(this.member); - }, + @action + remove(event) { + event?.preventDefault(); + this.removeAction(this.member); }, }); diff --git a/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js b/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js index 5456c75a79..d4b4bcb9de 100644 --- a/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js +++ b/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js @@ -43,7 +43,8 @@ export default Component.extend({ }, @action - prefillSettings(provider) { + prefillSettings(provider, event) { + event?.preventDefault(); this.form.setProperties(emailProviderDefaultSettings(provider, "smtp")); }, diff --git a/app/assets/javascripts/discourse/app/components/mobile-nav.js b/app/assets/javascripts/discourse/app/components/mobile-nav.js index bdbb2cc85a..f0ea3e6138 100644 --- a/app/assets/javascripts/discourse/app/components/mobile-nav.js +++ b/app/assets/javascripts/discourse/app/components/mobile-nav.js @@ -1,5 +1,6 @@ import { on } from "discourse-common/utils/decorators"; import Component from "@ember/component"; +import { action } from "@ember/object"; import { next } from "@ember/runloop"; import { inject as service } from "@ember/service"; import deprecated from "discourse-common/lib/deprecated"; @@ -56,27 +57,27 @@ export default Component.extend({ this.router.off("routeDidChange", this, this.currentRouteChanged); }, - actions: { - toggleExpanded() { - this.toggleProperty("expanded"); + @action + toggleExpanded(event) { + event?.preventDefault(); + this.toggleProperty("expanded"); - next(() => { - if (this.expanded) { - $(window) - .off("click.mobile-nav") - .on("click.mobile-nav", (e) => { - if (!this.element || this.isDestroying || this.isDestroyed) { - return; - } + next(() => { + if (this.expanded) { + $(window) + .off("click.mobile-nav") + .on("click.mobile-nav", (e) => { + if (!this.element || this.isDestroying || this.isDestroyed) { + return; + } - const expander = this.element.querySelector(".expander"); - if (expander && e.target !== expander) { - this.set("expanded", false); - $(window).off("click.mobile-nav"); - } - }); - } - }); - }, + const expander = this.element.querySelector(".expander"); + if (expander && e.target !== expander) { + this.set("expanded", false); + $(window).off("click.mobile-nav"); + } + }); + } + }); }, }); diff --git a/app/assets/javascripts/discourse/app/components/navigation-bar.js b/app/assets/javascripts/discourse/app/components/navigation-bar.js index ab9a5b9c71..5ede7b2bf4 100644 --- a/app/assets/javascripts/discourse/app/components/navigation-bar.js +++ b/app/assets/javascripts/discourse/app/components/navigation-bar.js @@ -1,5 +1,6 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators"; import Component from "@ember/component"; +import { action } from "@ember/object"; import DiscourseURL from "discourse/lib/url"; import FilterModeMixin from "discourse/mixins/filter-mode"; import { next } from "@ember/runloop"; @@ -61,33 +62,33 @@ export default Component.extend(FilterModeMixin, { DiscourseURL.appEvents.off("dom:clean", this, this.ensureDropClosed); }, - actions: { - toggleDrop() { - this.set("expanded", !this.expanded); + @action + toggleDrop(event) { + event?.preventDefault(); + this.set("expanded", !this.expanded); - if (this.expanded) { - DiscourseURL.appEvents.on("dom:clean", this, this.ensureDropClosed); + if (this.expanded) { + DiscourseURL.appEvents.on("dom:clean", this, this.ensureDropClosed); - next(() => { - if (!this.expanded) { - return; - } + next(() => { + if (!this.expanded) { + return; + } - $(this.element.querySelector(".drop a")).on("click", () => { - this.element.querySelector(".drop").style.display = "none"; + $(this.element.querySelector(".drop a")).on("click", () => { + this.element.querySelector(".drop").style.display = "none"; - next(() => { - this.ensureDropClosed(); - }); - return true; - }); - - $(window).on("click.navigation-bar", () => { + next(() => { this.ensureDropClosed(); - return true; }); + return true; }); - } - }, + + $(window).on("click.navigation-bar", () => { + this.ensureDropClosed(); + return true; + }); + }); + } }, }); diff --git a/app/assets/javascripts/discourse/app/components/reviewable-item.js b/app/assets/javascripts/discourse/app/components/reviewable-item.js index ff67e672a8..73c90d665a 100644 --- a/app/assets/javascripts/discourse/app/components/reviewable-item.js +++ b/app/assets/javascripts/discourse/app/components/reviewable-item.js @@ -7,7 +7,7 @@ import { classify, dasherize } from "@ember/string"; import discourseComputed, { bind } from "discourse-common/utils/decorators"; import optionalService from "discourse/lib/optional-service"; import { popupAjaxError } from "discourse/lib/ajax-error"; -import { set } from "@ember/object"; +import { action, set } from "@ember/object"; import showModal from "discourse/lib/show-modal"; let _components = {}; @@ -121,7 +121,7 @@ export default Component.extend({ }, @bind - _performConfirmed(action) { + _performConfirmed(performableAction) { let reviewable = this.reviewable; let performAction = () => { @@ -140,7 +140,7 @@ export default Component.extend({ }); return ajax( - `/review/${reviewable.id}/perform/${action.id}?version=${version}`, + `/review/${reviewable.id}/perform/${performableAction.id}?version=${version}`, { type: "PUT", data, @@ -173,13 +173,16 @@ export default Component.extend({ .finally(() => this.set("updating", false)); }; - if (action.client_action) { - let actionMethod = this[`client${classify(action.client_action)}`]; + if (performableAction.client_action) { + let actionMethod = + this[`client${classify(performableAction.client_action)}`]; if (actionMethod) { return actionMethod.call(this, reviewable, performAction); } else { // eslint-disable-next-line no-console - console.error(`No handler for ${action.client_action} found`); + console.error( + `No handler for ${performableAction.client_action} found` + ); return; } } else { @@ -209,14 +212,16 @@ export default Component.extend({ } }, - actions: { - explainReviewable(reviewable) { - showModal("explain-reviewable", { - title: "review.explain.title", - model: reviewable, - }); - }, + @action + explainReviewable(reviewable, event) { + event?.preventDefault(); + showModal("explain-reviewable", { + title: "review.explain.title", + model: reviewable, + }); + }, + actions: { edit() { this.set("editing", true); this.set("_updates", { payload: {} }); @@ -259,18 +264,18 @@ export default Component.extend({ set(this._updates, fieldId, event.target.value); }, - perform(action) { + perform(performableAction) { if (this.updating) { return; } - let msg = action.get("confirm_message"); - let requireRejectReason = action.get("require_reject_reason"); - let customModal = action.get("custom_modal"); + let msg = performableAction.get("confirm_message"); + let requireRejectReason = performableAction.get("require_reject_reason"); + let customModal = performableAction.get("custom_modal"); if (msg) { bootbox.confirm(msg, (answer) => { if (answer) { - return this._performConfirmed(action); + return this._performConfirmed(performableAction); } }); } else if (requireRejectReason) { @@ -279,7 +284,7 @@ export default Component.extend({ model: this.reviewable, }).setProperties({ performConfirmed: this._performConfirmed, - action, + action: performableAction, }); } else if (customModal) { showModal(customModal, { @@ -287,10 +292,10 @@ export default Component.extend({ model: this.reviewable, }).setProperties({ performConfirmed: this._performConfirmed, - action, + action: performableAction, }); } else { - return this._performConfirmed(action); + return this._performConfirmed(performableAction); } }, }, diff --git a/app/assets/javascripts/discourse/app/components/reviewable-post-edits.js b/app/assets/javascripts/discourse/app/components/reviewable-post-edits.js index 84c27f152f..75b8692a49 100644 --- a/app/assets/javascripts/discourse/app/components/reviewable-post-edits.js +++ b/app/assets/javascripts/discourse/app/components/reviewable-post-edits.js @@ -1,5 +1,6 @@ import Component from "@ember/component"; import discourseComputed from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; import { gt } from "@ember/object/computed"; import { historyHeat } from "discourse/widgets/post-edits-indicator"; import { longDate } from "discourse/lib/formatter"; @@ -18,18 +19,18 @@ export default Component.extend({ return longDate(updatedAt); }, - actions: { - showEditHistory() { - let postId = this.get("reviewable.post_id"); - this.store.find("post", postId).then((post) => { - let historyController = showModal("history", { - model: post, - modalClass: "history-modal", - }); - historyController.refresh(postId, "latest"); - historyController.set("post", post); - historyController.set("topicController", null); + @action + showEditHistory(event) { + event?.preventDefault(); + let postId = this.get("reviewable.post_id"); + this.store.find("post", postId).then((post) => { + let historyController = showModal("history", { + model: post, + modalClass: "history-modal", }); - }, + historyController.refresh(postId, "latest"); + historyController.set("post", post); + historyController.set("topicController", null); + }); }, }); diff --git a/app/assets/javascripts/discourse/app/components/reviewable-queued-post.js b/app/assets/javascripts/discourse/app/components/reviewable-queued-post.js index e40ab39ef7..d06ddf3c1c 100644 --- a/app/assets/javascripts/discourse/app/components/reviewable-queued-post.js +++ b/app/assets/javascripts/discourse/app/components/reviewable-queued-post.js @@ -1,10 +1,11 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; import showModal from "discourse/lib/show-modal"; export default Component.extend({ - actions: { - showRawEmail() { - showModal("raw-email").set("rawEmail", this.reviewable.payload.raw_email); - }, + @action + showRawEmail(event) { + event?.preventDefault(); + showModal("raw-email").set("rawEmail", this.reviewable.payload.raw_email); }, }); diff --git a/app/assets/javascripts/discourse/app/components/search-result-entry.js b/app/assets/javascripts/discourse/app/components/search-result-entry.js index 9312174ed1..cf13405a65 100644 --- a/app/assets/javascripts/discourse/app/components/search-result-entry.js +++ b/app/assets/javascripts/discourse/app/components/search-result-entry.js @@ -1,6 +1,7 @@ import Component from "@ember/component"; import { action } from "@ember/object"; import { logSearchLinkClick } from "discourse/lib/search"; +import { modKeysPressed } from "discourse/lib/utilities"; export default Component.extend({ tagName: "div", @@ -10,7 +11,11 @@ export default Component.extend({ role: "listitem", @action - logClick(topicId) { + logClick(topicId, event) { + // Avoid click logging when any modifier keys are pressed. + if (event && modKeysPressed(event).length > 0) { + return false; + } if (this.searchLogId && topicId) { logSearchLinkClick({ searchLogId: this.searchLogId, diff --git a/app/assets/javascripts/discourse/app/components/second-factor-form.js b/app/assets/javascripts/discourse/app/components/second-factor-form.js index 3f0c40b381..e208a5f314 100644 --- a/app/assets/javascripts/discourse/app/components/second-factor-form.js +++ b/app/assets/javascripts/discourse/app/components/second-factor-form.js @@ -1,4 +1,5 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; import I18n from "I18n"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; import discourseComputed from "discourse-common/utils/decorators"; @@ -48,15 +49,15 @@ export default Component.extend({ ); }, - actions: { - toggleSecondFactorMethod() { - const secondFactorMethod = this.secondFactorMethod; - this.set("secondFactorToken", ""); - if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) { - this.set("secondFactorMethod", SECOND_FACTOR_METHODS.BACKUP_CODE); - } else { - this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP); - } - }, + @action + toggleSecondFactorMethod(event) { + event?.preventDefault(); + const secondFactorMethod = this.secondFactorMethod; + this.set("secondFactorToken", ""); + if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) { + this.set("secondFactorMethod", SECOND_FACTOR_METHODS.BACKUP_CODE); + } else { + this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP); + } }, }); diff --git a/app/assets/javascripts/discourse/app/components/security-key-form.js b/app/assets/javascripts/discourse/app/components/security-key-form.js index bd6d03f9f4..dcc0725113 100644 --- a/app/assets/javascripts/discourse/app/components/security-key-form.js +++ b/app/assets/javascripts/discourse/app/components/security-key-form.js @@ -1,12 +1,13 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; export default Component.extend({ - actions: { - useAnotherMethod() { - this.set("showSecurityKey", false); - this.set("showSecondFactor", true); - this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP); - }, + @action + useAnotherMethod(event) { + event?.preventDefault(); + this.set("showSecurityKey", false); + this.set("showSecondFactor", true); + this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP); }, }); diff --git a/app/assets/javascripts/discourse/app/components/signup-cta.js b/app/assets/javascripts/discourse/app/components/signup-cta.js index 5ea99dffe9..a4d80fd611 100644 --- a/app/assets/javascripts/discourse/app/components/signup-cta.js +++ b/app/assets/javascripts/discourse/app/components/signup-cta.js @@ -1,15 +1,19 @@ import Component from "@ember/component"; import discourseLater from "discourse-common/lib/later"; +import { action } from "@ember/object"; import { on } from "@ember/object/evented"; export default Component.extend({ action: "showCreateAccount", + @action + neverShow(event) { + event?.preventDefault(); + this.keyValueStore.setItem("anon-cta-never", "t"); + this.session.set("showSignupCta", false); + }, + actions: { - neverShow() { - this.keyValueStore.setItem("anon-cta-never", "t"); - this.session.set("showSignupCta", false); - }, hideForSession() { this.session.set("hideSignupCta", true); this.keyValueStore.setItem("anon-cta-hidden", Date.now()); diff --git a/app/assets/javascripts/discourse/app/components/tag-info.js b/app/assets/javascripts/discourse/app/components/tag-info.js index 6cb2a35dba..21da9a92f1 100644 --- a/app/assets/javascripts/discourse/app/components/tag-info.js +++ b/app/assets/javascripts/discourse/app/components/tag-info.js @@ -7,6 +7,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import { isEmpty } from "@ember/utils"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; export default Component.extend({ dialog: service(), @@ -76,19 +77,49 @@ export default Component.extend({ .catch(popupAjaxError); }, + @action + edit(event) { + event?.preventDefault(); + this.setProperties({ + editing: true, + newTagName: this.tag.id, + newTagDescription: this.tagInfo.description, + }); + }, + + @action + unlinkSynonym(tag, event) { + event?.preventDefault(); + ajax(`/tag/${this.tagInfo.name}/synonyms/${tag.id}`, { + type: "DELETE", + }) + .then(() => this.tagInfo.synonyms.removeObject(tag)) + .catch(popupAjaxError); + }, + + @action + deleteSynonym(tag, event) { + event?.preventDefault(); + bootbox.confirm( + I18n.t("tagging.delete_synonym_confirm", { tag_name: tag.text }), + (result) => { + if (!result) { + return; + } + + tag + .destroyRecord() + .then(() => this.tagInfo.synonyms.removeObject(tag)) + .catch(popupAjaxError); + } + ); + }, + actions: { toggleEditControls() { this.toggleProperty("showEditControls"); }, - edit() { - this.setProperties({ - editing: true, - newTagName: this.tag.id, - newTagDescription: this.tagInfo.description, - }); - }, - cancelEditing() { this.set("editing", false); }, @@ -114,30 +145,6 @@ export default Component.extend({ this.deleteAction(this.tagInfo); }, - unlinkSynonym(tag) { - ajax(`/tag/${this.tagInfo.name}/synonyms/${tag.id}`, { - type: "DELETE", - }) - .then(() => this.tagInfo.synonyms.removeObject(tag)) - .catch(popupAjaxError); - }, - - deleteSynonym(tag) { - bootbox.confirm( - I18n.t("tagging.delete_synonym_confirm", { tag_name: tag.text }), - (result) => { - if (!result) { - return; - } - - tag - .destroyRecord() - .then(() => this.tagInfo.synonyms.removeObject(tag)) - .catch(popupAjaxError); - } - ); - }, - addSynonyms() { bootbox.confirm( I18n.t("tagging.add_synonyms_explanation", { 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 972e824173..2d6ce3e18f 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -251,6 +251,7 @@ export default Component.extend({ if (wantsNewWindow(e)) { return true; } + e.preventDefault(); return this.navigateToTopic(topic, e.target.getAttribute("href")); } @@ -264,6 +265,7 @@ export default Component.extend({ if (wantsNewWindow(e)) { return true; } + e.preventDefault(); return this.navigateToTopic(topic, topic.lastUnreadUrl); } diff --git a/app/assets/javascripts/discourse/app/components/user-card-contents.js b/app/assets/javascripts/discourse/app/components/user-card-contents.js index 930f77a8c1..34e3147745 100644 --- a/app/assets/javascripts/discourse/app/components/user-card-contents.js +++ b/app/assets/javascripts/discourse/app/components/user-card-contents.js @@ -1,4 +1,4 @@ -import EmberObject, { set } from "@ember/object"; +import EmberObject, { action, set } from "@ember/object"; import { alias, and, gt, gte, not, or } from "@ember/object/computed"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; import { propertyNotEqual, setting } from "discourse/lib/computed"; @@ -14,7 +14,7 @@ import { isEmpty } from "@ember/utils"; import { prioritizeNameInUx } from "discourse/lib/settings"; import { dasherize } from "@ember/string"; import { emojiUnescape } from "discourse/lib/text"; -import { escapeExpression } from "discourse/lib/utilities"; +import { escapeExpression, modKeysPressed } from "discourse/lib/utilities"; export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, { elementId: "user-card", @@ -220,6 +220,18 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, { this._close(); }, + @action + handleShowUser(user, event) { + if (event && modKeysPressed(event).length > 0) { + return false; + } + event?.preventDefault(); + // Invokes `showUser` argument. Convert to `this.args.showUser` when + // refactoring this to a glimmer component. + this.showUser(user); + this._close(); + }, + actions: { close() { this._close(); @@ -247,9 +259,8 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, { this._close(); }, - showUser(username) { - this.showUser(username); - this._close(); + showUser(user) { + this.handleShowUser(user); }, checkEmail(user) { diff --git a/app/assets/javascripts/discourse/app/controllers/auth-token.js b/app/assets/javascripts/discourse/app/controllers/auth-token.js index 2d8c91157a..52ddce5f4d 100644 --- a/app/assets/javascripts/discourse/app/controllers/auth-token.js +++ b/app/assets/javascripts/discourse/app/controllers/auth-token.js @@ -1,6 +1,7 @@ import Controller from "@ember/controller"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { ajax } from "discourse/lib/ajax"; +import { action } from "@ember/object"; import { next } from "@ember/runloop"; import { userPath } from "discourse/lib/url"; @@ -17,11 +18,13 @@ export default Controller.extend(ModalFunctionality, { }); }, - actions: { - toggleExpanded() { - this.set("expanded", !this.expanded); - }, + @action + toggleExpanded(event) { + event?.preventDefault(); + this.set("expanded", !this.expanded); + }, + actions: { highlightSecure() { this.send("closeModal"); diff --git a/app/assets/javascripts/discourse/app/controllers/avatar-selector.js b/app/assets/javascripts/discourse/app/controllers/avatar-selector.js index 8373b7a106..d63f464c2a 100644 --- a/app/assets/javascripts/discourse/app/controllers/avatar-selector.js +++ b/app/assets/javascripts/discourse/app/controllers/avatar-selector.js @@ -1,4 +1,5 @@ import Controller from "@ember/controller"; +import { action } from "@ember/object"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { ajax } from "discourse/lib/ajax"; import { allowsImages } from "discourse/lib/uploads"; @@ -132,6 +133,15 @@ export default Controller.extend(ModalFunctionality, { ); }, + @action + selectAvatar(url, event) { + event?.preventDefault(); + this.user + .selectAvatar(url) + .then(() => window.location.reload()) + .catch(popupAjaxError); + }, + actions: { uploadComplete() { this.set("selected", "custom"); @@ -159,13 +169,6 @@ export default Controller.extend(ModalFunctionality, { .finally(() => this.set("gravatarRefreshDisabled", false)); }, - selectAvatar(url) { - this.user - .selectAvatar(url) - .then(() => window.location.reload()) - .catch(popupAjaxError); - }, - saveAvatarSelection() { const selectedUploadId = this.selectedUploadId; const type = this.selected; diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index 3463f6f1d7..a25874f18d 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -23,7 +23,7 @@ import { buildQuote } from "discourse/lib/quote"; import deprecated from "discourse-common/lib/deprecated"; import discourseDebounce from "discourse-common/lib/debounce"; import { emojiUnescape } from "discourse/lib/text"; -import { escapeExpression } from "discourse/lib/utilities"; +import { escapeExpression, modKeysPressed } from "discourse/lib/utilities"; import { getOwner } from "discourse-common/lib/get-owner"; import getURL from "discourse-common/lib/get-url"; import { isEmpty } from "@ember/utils"; @@ -508,11 +508,35 @@ export default Controller.extend({ this.set("model.showFullScreenExitPrompt", false); }, - actions: { - togglePreview() { - this.toggleProperty("showPreview"); - }, + @action + async cancel(event) { + event?.preventDefault(); + await this.cancelComposer(); + }, + @action + cancelUpload(event) { + event?.preventDefault(); + this.set("model.uploadCancelled", true); + }, + + @action + togglePreview(event) { + event?.preventDefault(); + this.toggleProperty("showPreview"); + }, + + @action + viewNewReply(event) { + if (event && modKeysPressed(event).length > 0) { + return false; + } + event?.preventDefault(); + DiscourseURL.routeTo(this.get("model.createdPost.url")); + this.close(); + }, + + actions: { closeComposer() { this.close(); }, @@ -543,10 +567,6 @@ export default Controller.extend({ }); }, - cancelUpload() { - this.set("model.uploadCancelled", true); - }, - onPopupMenuAction(menuAction) { this.send(menuAction); }, @@ -707,10 +727,6 @@ export default Controller.extend({ this.set("model.loading", false); }, - async cancel() { - await this.cancelComposer(); - }, - save(ignore, event) { this.save(false, { jump: @@ -1275,12 +1291,6 @@ export default Controller.extend({ } }, - viewNewReply() { - DiscourseURL.routeTo(this.get("model.createdPost.url")); - this.close(); - return false; - }, - async destroyDraft(draftSequence = null) { const key = this.get("model.draftKey"); if (!key) { diff --git a/app/assets/javascripts/discourse/app/controllers/discovery/categories.js b/app/assets/javascripts/discourse/app/controllers/discovery/categories.js index c46f41c1c8..1e07f67cb9 100644 --- a/app/assets/javascripts/discourse/app/controllers/discovery/categories.js +++ b/app/assets/javascripts/discourse/app/controllers/discovery/categories.js @@ -1,5 +1,6 @@ import DiscoveryController from "discourse/controllers/discovery"; import { inject as controller } from "@ember/controller"; +import { action } from "@ember/object"; import { dasherize } from "@ember/string"; import discourseComputed from "discourse-common/utils/decorators"; import { reads } from "@ember/object/computed"; @@ -50,17 +51,19 @@ export default DiscoveryController.extend({ : style; return dasherize(componentName); }, + + @action + showInserted(event) { + event?.preventDefault(); + const tracker = this.topicTrackingState; + // Move inserted into topics + this.model.loadBefore(tracker.get("newIncoming"), true); + tracker.resetTracking(); + }, + actions: { refresh() { this.send("triggerRefresh"); }, - showInserted() { - const tracker = this.topicTrackingState; - - // Move inserted into topics - this.model.loadBefore(tracker.get("newIncoming"), true); - tracker.resetTracking(); - return false; - }, }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/discovery/topics.js b/app/assets/javascripts/discourse/app/controllers/discovery/topics.js index a0037f4da5..3e90fd42a8 100644 --- a/app/assets/javascripts/discourse/app/controllers/discovery/topics.js +++ b/app/assets/javascripts/discourse/app/controllers/discovery/topics.js @@ -63,6 +63,17 @@ const controllerOpts = { return this._isFilterPage(filter, "new") && topicsLength > 0; }, + // Show newly inserted topics + @action + showInserted(event) { + event?.preventDefault(); + const tracker = this.topicTrackingState; + + // Move inserted into topics + this.model.loadBefore(tracker.get("newIncoming"), true); + tracker.resetTracking(); + }, + actions: { changeSort() { deprecated( @@ -72,16 +83,6 @@ const controllerOpts = { return routeAction("changeSort", this.router._router, ...arguments)(); }, - // Show newly inserted topics - showInserted() { - const tracker = this.topicTrackingState; - - // Move inserted into topics - this.model.loadBefore(tracker.get("newIncoming"), true); - tracker.resetTracking(); - return false; - }, - refresh(options = { skipResettingParams: [] }) { const filter = this.get("model.filter"); this.send("resetParams", options.skipResettingParams); diff --git a/app/assets/javascripts/discourse/app/controllers/full-page-search.js b/app/assets/javascripts/discourse/app/controllers/full-page-search.js index e082ca9d29..a7cf017c6a 100644 --- a/app/assets/javascripts/discourse/app/controllers/full-page-search.js +++ b/app/assets/javascripts/discourse/app/controllers/full-page-search.js @@ -14,6 +14,7 @@ import I18n from "I18n"; import { ajax } from "discourse/lib/ajax"; import { escapeExpression } from "discourse/lib/utilities"; import { isEmpty } from "@ember/utils"; +import { action } from "@ember/object"; import { gt, or } from "@ember/object/computed"; import { scrollTop } from "discourse/mixins/scroll-top"; import { setTransient } from "discourse/lib/page-tracker"; @@ -391,22 +392,24 @@ export default Controller.extend({ } }, - actions: { - createTopic(searchTerm) { - let topicCategory; - if (searchTerm.includes("category:")) { - const match = searchTerm.match(/category:(\S*)/); - if (match && match[1]) { - topicCategory = match[1]; - } + @action + createTopic(searchTerm, event) { + event?.preventDefault(); + let topicCategory; + if (searchTerm.includes("category:")) { + const match = searchTerm.match(/category:(\S*)/); + if (match && match[1]) { + topicCategory = match[1]; } - this.composer.open({ - action: Composer.CREATE_TOPIC, - draftKey: Composer.NEW_TOPIC_KEY, - topicCategory, - }); - }, + } + this.composer.open({ + action: Composer.CREATE_TOPIC, + draftKey: Composer.NEW_TOPIC_KEY, + topicCategory, + }); + }, + actions: { selectAll() { this.selected.addObjects(this.get("model.posts").mapBy("topic")); diff --git a/app/assets/javascripts/discourse/app/controllers/history.js b/app/assets/javascripts/discourse/app/controllers/history.js index 2540c78b54..00e64f86da 100644 --- a/app/assets/javascripts/discourse/app/controllers/history.js +++ b/app/assets/javascripts/discourse/app/controllers/history.js @@ -1,3 +1,4 @@ +import { action } from "@ember/object"; import { alias, equal, gt, not, or } from "@ember/object/computed"; import discourseComputed, { observes, @@ -313,6 +314,24 @@ export default Controller.extend(ModalFunctionality, { } }, + @action + displayInline(event) { + event?.preventDefault(); + this.set("viewMode", "inline"); + }, + + @action + displaySideBySide(event) { + event?.preventDefault(); + this.set("viewMode", "side_by_side"); + }, + + @action + displaySideBySideMarkdown(event) { + event?.preventDefault(); + this.set("viewMode", "side_by_side_markdown"); + }, + actions: { loadFirstVersion() { this.refresh(this.get("model.post_id"), this.get("model.first_revision")); @@ -345,15 +364,5 @@ export default Controller.extend(ModalFunctionality, { revertToVersion() { this.revert(this.post, this.get("model.current_revision")); }, - - displayInline() { - this.set("viewMode", "inline"); - }, - displaySideBySide() { - this.set("viewMode", "side_by_side"); - }, - displaySideBySideMarkdown() { - this.set("viewMode", "side_by_side_markdown"); - }, }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js index 1a5a4ecc7f..401c85f354 100644 --- a/app/assets/javascripts/discourse/app/controllers/login.js +++ b/app/assets/javascripts/discourse/app/controllers/login.js @@ -3,7 +3,7 @@ import { alias, not, or, readOnly } from "@ember/object/computed"; import { areCookiesEnabled, escapeExpression } from "discourse/lib/utilities"; import cookie, { removeCookie } from "discourse/lib/cookie"; import { next, schedule } from "@ember/runloop"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; @@ -133,7 +133,66 @@ export default Controller.extend(ModalFunctionality, { return canLoginLocalWithEmail; }, + @action + emailLogin(event) { + event?.preventDefault(); + + if (this.processingEmailLink) { + return; + } + + if (isEmpty(this.loginName)) { + this.flash(I18n.t("login.blank_username"), "info"); + return; + } + + this.set("processingEmailLink", true); + + ajax("/u/email-login", { + data: { login: this.loginName.trim() }, + type: "POST", + }) + .then((data) => { + const loginName = escapeExpression(this.loginName); + const isEmail = loginName.match(/@/); + let key = `email_login.complete_${isEmail ? "email" : "username"}`; + if (data.user_found === false) { + this.flash( + I18n.t(`${key}_not_found`, { + email: loginName, + username: loginName, + }), + "error" + ); + } else { + let postfix = data.hide_taken ? "" : "_found"; + this.flash( + I18n.t(`${key}${postfix}`, { + email: loginName, + username: loginName, + }) + ); + } + }) + .catch((e) => this.flash(extractError(e), "error")) + .finally(() => this.set("processingEmailLink", false)); + }, + + @action + handleForgotPassword(event) { + event?.preventDefault(); + const forgotPasswordController = this.forgotPassword; + if (forgotPasswordController) { + forgotPasswordController.set("accountEmailOrUsername", this.loginName); + } + this.send("showForgotPassword"); + }, + actions: { + forgotPassword() { + this.handleForgotPassword(); + }, + login() { if (this.loginDisabled) { return; @@ -297,56 +356,6 @@ export default Controller.extend(ModalFunctionality, { this.send("showCreateAccount"); }, - forgotPassword() { - const forgotPasswordController = this.forgotPassword; - if (forgotPasswordController) { - forgotPasswordController.set("accountEmailOrUsername", this.loginName); - } - this.send("showForgotPassword"); - }, - - emailLogin() { - if (this.processingEmailLink) { - return; - } - - if (isEmpty(this.loginName)) { - this.flash(I18n.t("login.blank_username"), "info"); - return; - } - - this.set("processingEmailLink", true); - - ajax("/u/email-login", { - data: { login: this.loginName.trim() }, - type: "POST", - }) - .then((data) => { - const loginName = escapeExpression(this.loginName); - const isEmail = loginName.match(/@/); - let key = `email_login.complete_${isEmail ? "email" : "username"}`; - if (data.user_found === false) { - this.flash( - I18n.t(`${key}_not_found`, { - email: loginName, - username: loginName, - }), - "error" - ); - } else { - let postfix = data.hide_taken ? "" : "_found"; - this.flash( - I18n.t(`${key}${postfix}`, { - email: loginName, - username: loginName, - }) - ); - } - }) - .catch((e) => this.flash(extractError(e), "error")) - .finally(() => this.set("processingEmailLink", false)); - }, - authenticateSecurityKey() { getWebauthnCredential( this.securityKeyChallenge, diff --git a/app/assets/javascripts/discourse/app/controllers/password-reset.js b/app/assets/javascripts/discourse/app/controllers/password-reset.js index bae85083a8..472a69005d 100644 --- a/app/assets/javascripts/discourse/app/controllers/password-reset.js +++ b/app/assets/javascripts/discourse/app/controllers/password-reset.js @@ -1,4 +1,5 @@ import DiscourseURL, { userPath } from "discourse/lib/url"; +import { action } from "@ember/object"; import { alias, or, readOnly } from "@ember/object/computed"; import Controller from "@ember/controller"; import I18n from "I18n"; @@ -8,6 +9,7 @@ import { ajax } from "discourse/lib/ajax"; import discourseComputed from "discourse-common/utils/decorators"; import getURL from "discourse-common/lib/get-url"; import { getWebauthnCredential } from "discourse/lib/webauthn"; +import { modKeysPressed } from "discourse/lib/utilities"; export default Controller.extend(PasswordValidation, { isDeveloper: alias("model.is_developer"), @@ -46,6 +48,16 @@ export default Controller.extend(PasswordValidation, { lockImageUrl: getURL("/images/lock.svg"), + @action + done(event) { + if (event && modKeysPressed(event).length > 0) { + return false; + } + event?.preventDefault(); + this.set("redirected", true); + DiscourseURL.redirectTo(this.redirectTo || "/"); + }, + actions: { submit() { ajax({ @@ -126,10 +138,5 @@ export default Controller.extend(PasswordValidation, { } ); }, - - done() { - this.set("redirected", true); - DiscourseURL.redirectTo(this.redirectTo || "/"); - }, }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/account.js b/app/assets/javascripts/discourse/app/controllers/preferences/account.js index 6c3051d452..94ba197cfd 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/account.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/account.js @@ -2,7 +2,7 @@ import { gt, not, or } from "@ember/object/computed"; import { propertyNotEqual, setting } from "discourse/lib/computed"; import CanCheckEmails from "discourse/mixins/can-check-emails"; import Controller from "@ember/controller"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { findAll } from "discourse/models/login-method"; @@ -132,6 +132,20 @@ export default Controller.extend(CanCheckEmails, { return findAll().length > 0; }, + @action + resendConfirmationEmail(email, event) { + event?.preventDefault(); + email.set("resending", true); + this.model + .addEmail(email.email) + .then(() => { + email.set("resent", true); + }) + .finally(() => { + email.set("resending", false); + }); + }, + actions: { save() { this.set("saved", false); @@ -157,18 +171,6 @@ export default Controller.extend(CanCheckEmails, { this.model.destroyEmail(email); }, - resendConfirmationEmail(email) { - email.set("resending", true); - this.model - .addEmail(email.email) - .then(() => { - email.set("resent", true); - }) - .finally(() => { - email.set("resending", false); - }); - }, - delete() { this.dialog.alert({ message: I18n.t("user.delete_account_confirm"), diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js b/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js index 946903a568..fc5293054a 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js @@ -3,6 +3,7 @@ import CanCheckEmails from "discourse/mixins/can-check-emails"; import Controller from "@ember/controller"; import I18n from "I18n"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; +import { action } from "@ember/object"; import { alias } from "@ember/object/computed"; import bootbox from "bootbox"; import discourseComputed from "discourse-common/utils/decorators"; @@ -91,6 +92,27 @@ export default Controller.extend(CanCheckEmails, { this.set("dirty", true); }, + @action + resetPassword(event) { + event?.preventDefault(); + + this.setProperties({ + resetPasswordLoading: true, + resetPasswordProgress: "", + }); + + return this.model + .changePassword() + .then(() => { + this.set( + "resetPasswordProgress", + I18n.t("user.change_password.success") + ); + }) + .catch(popupAjaxError) + .finally(() => this.set("resetPasswordLoading", false)); + }, + actions: { confirmPassword() { if (!this.password) { @@ -101,24 +123,6 @@ export default Controller.extend(CanCheckEmails, { this.set("password", null); }, - resetPassword() { - this.setProperties({ - resetPasswordLoading: true, - resetPasswordProgress: "", - }); - - return this.model - .changePassword() - .then(() => { - this.set( - "resetPasswordProgress", - I18n.t("user.change_password.success") - ); - }) - .catch(popupAjaxError) - .finally(() => this.set("resetPasswordLoading", false)); - }, - disableAllSecondFactors() { if (this.loading) { return; diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/security.js b/app/assets/javascripts/discourse/app/controllers/preferences/security.js index c1e7dc52c0..800d597c65 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/security.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/security.js @@ -1,4 +1,5 @@ import Controller from "@ember/controller"; +import { action } from "@ember/object"; import { gt } from "@ember/object/computed"; import discourseComputed from "discourse-common/utils/decorators"; import { ajax } from "discourse/lib/ajax"; @@ -51,6 +52,56 @@ export default Controller.extend(CanCheckEmails, { DEFAULT_AUTH_TOKENS_COUNT ), + @action + changePassword(event) { + event?.preventDefault(); + if (!this.passwordProgress) { + this.set("passwordProgress", I18n.t("user.change_password.in_progress")); + return this.model + .changePassword() + .then(() => { + // password changed + this.setProperties({ + changePasswordProgress: false, + passwordProgress: I18n.t("user.change_password.success"), + }); + }) + .catch(() => { + // password failed to change + this.setProperties({ + changePasswordProgress: false, + passwordProgress: I18n.t("user.change_password.error"), + }); + }); + } + }, + + @action + toggleShowAllAuthTokens(event) { + event?.preventDefault(); + this.toggleProperty("showAllAuthTokens"); + }, + + @action + revokeAuthToken(token, event) { + event?.preventDefault(); + ajax( + userPath( + `${this.get("model.username_lower")}/preferences/revoke-auth-token` + ), + { + type: "POST", + data: token ? { token_id: token.id } : {}, + } + ) + .then(() => { + if (!token) { + logout(); + } // All sessions revoked + }) + .catch(popupAjaxError); + }, + actions: { save() { this.set("saved", false); @@ -60,53 +111,6 @@ export default Controller.extend(CanCheckEmails, { .catch(popupAjaxError); }, - changePassword() { - if (!this.passwordProgress) { - this.set( - "passwordProgress", - I18n.t("user.change_password.in_progress") - ); - return this.model - .changePassword() - .then(() => { - // password changed - this.setProperties({ - changePasswordProgress: false, - passwordProgress: I18n.t("user.change_password.success"), - }); - }) - .catch(() => { - // password failed to change - this.setProperties({ - changePasswordProgress: false, - passwordProgress: I18n.t("user.change_password.error"), - }); - }); - } - }, - - toggleShowAllAuthTokens() { - this.toggleProperty("showAllAuthTokens"); - }, - - revokeAuthToken(token) { - ajax( - userPath( - `${this.get("model.username_lower")}/preferences/revoke-auth-token` - ), - { - type: "POST", - data: token ? { token_id: token.id } : {}, - } - ) - .then(() => { - if (!token) { - logout(); - } // All sessions revoked - }) - .catch(popupAjaxError); - }, - showToken(token) { showModal("auth-token", { model: token }); }, diff --git a/app/assets/javascripts/discourse/app/controllers/second-factor-add-totp.js b/app/assets/javascripts/discourse/app/controllers/second-factor-add-totp.js index bdc1f3511d..2ed9127535 100644 --- a/app/assets/javascripts/discourse/app/controllers/second-factor-add-totp.js +++ b/app/assets/javascripts/discourse/app/controllers/second-factor-add-totp.js @@ -1,4 +1,5 @@ import Controller from "@ember/controller"; +import { action } from "@ember/object"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; @@ -40,9 +41,15 @@ export default Controller.extend(ModalFunctionality, { .finally(() => this.set("loading", false)); }, + @action + enableShowSecondFactorKey(event) { + event?.preventDefault(); + this.set("showSecondFactorKey", true); + }, + actions: { showSecondFactorKey() { - this.set("showSecondFactorKey", true); + this.enableShowSecondFactorKey(); }, enableSecondFactor() { diff --git a/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js b/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js index 8e88afdf95..f4199f9ee9 100644 --- a/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js +++ b/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js @@ -212,7 +212,8 @@ export default Controller.extend({ }, @action - useAnotherMethod(newMethod) { + useAnotherMethod(newMethod, event) { + event?.preventDefault(); this.set("userSelectedMethod", newMethod); }, diff --git a/app/assets/javascripts/discourse/app/controllers/tag-show.js b/app/assets/javascripts/discourse/app/controllers/tag-show.js index 619c5e860d..2f9c493516 100644 --- a/app/assets/javascripts/discourse/app/controllers/tag-show.js +++ b/app/assets/javascripts/discourse/app/controllers/tag-show.js @@ -110,7 +110,8 @@ export default DiscoverySortableController.extend( }, @action - showInserted() { + showInserted(event) { + event?.preventDefault(); const tracker = this.topicTrackingState; this.list.loadBefore(tracker.newIncoming, true); tracker.resetTracking(); diff --git a/app/assets/javascripts/discourse/app/controllers/tags-index.js b/app/assets/javascripts/discourse/app/controllers/tags-index.js index ec56d24003..eff2f1d30b 100644 --- a/app/assets/javascripts/discourse/app/controllers/tags-index.js +++ b/app/assets/javascripts/discourse/app/controllers/tags-index.js @@ -1,3 +1,4 @@ +import { action } from "@ember/object"; import { alias, notEmpty } from "@ember/object/computed"; import Controller from "@ember/controller"; import I18n from "I18n"; @@ -41,23 +42,27 @@ export default Controller.extend({ }; }, + @action + sortByCount(event) { + event?.preventDefault(); + this.setProperties({ + sortProperties: ["totalCount:desc", "id"], + sortedByCount: true, + sortedByName: false, + }); + }, + + @action + sortById(event) { + event?.preventDefault(); + this.setProperties({ + sortProperties: ["id"], + sortedByCount: false, + sortedByName: true, + }); + }, + actions: { - sortByCount() { - this.setProperties({ - sortProperties: ["totalCount:desc", "id"], - sortedByCount: true, - sortedByName: false, - }); - }, - - sortById() { - this.setProperties({ - sortProperties: ["id"], - sortedByCount: false, - sortedByName: true, - }); - }, - showUploader() { showModal("tag-upload"); }, diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index aace5530cb..7b1da2f654 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -19,7 +19,7 @@ import { ajax } from "discourse/lib/ajax"; import { bufferedProperty } from "discourse/mixins/buffered-content"; import { buildQuote } from "discourse/lib/quote"; import { deepMerge } from "discourse-common/lib/object"; -import { escapeExpression } from "discourse/lib/utilities"; +import { escapeExpression, modKeysPressed } from "discourse/lib/utilities"; import { extractLinkMeta } from "discourse/lib/render-topic-featured-link"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { inject as service } from "@ember/service"; @@ -313,6 +313,58 @@ export default Controller.extend(bufferedProperty("model"), { }); }, + @action + editTopic(event) { + event?.preventDefault(); + if (this.get("model.details.can_edit")) { + this.set("editingTopic", true); + } + }, + + @action + jumpTop(event) { + if (event && modKeysPressed(event).length > 0) { + return false; + } + event?.preventDefault(); + DiscourseURL.routeTo(this.get("model.firstPostUrl"), { + skipIfOnScreen: false, + keepFilter: true, + }); + }, + + @action + removeFeaturedLink(event) { + event?.preventDefault(); + this.set("buffered.featured_link", null); + }, + + @action + selectAll(event) { + event?.preventDefault(); + const smallActionsPostIds = this._smallActionPostIds(); + this.set("selectedPostIds", [ + ...this.get("model.postStream.stream").filter( + (postId) => !smallActionsPostIds.has(postId) + ), + ]); + this._forceRefreshPostStream(); + }, + + @action + deselectAll(event) { + event?.preventDefault(); + this.set("selectedPostIds", []); + this._forceRefreshPostStream(); + }, + + @action + toggleMultiSelect(event) { + event?.preventDefault(); + this.toggleProperty("multiSelect"); + this._forceRefreshPostStream(); + }, + actions: { topicCategoryChanged(categoryId) { this.set("buffered.category_id", categoryId); @@ -822,13 +874,6 @@ export default Controller.extend(bufferedProperty("model"), { this._jumpToPostNumber(postNumber); }, - jumpTop() { - DiscourseURL.routeTo(this.get("model.firstPostUrl"), { - skipIfOnScreen: false, - keepFilter: true, - }); - }, - jumpBottom() { // When a topic only has one lengthy post const jumpEnd = this.model.highest_post_number === 1 ? true : false; @@ -859,26 +904,6 @@ export default Controller.extend(bufferedProperty("model"), { this._jumpToPostId(postId); }, - toggleMultiSelect() { - this.toggleProperty("multiSelect"); - this._forceRefreshPostStream(); - }, - - selectAll() { - const smallActionsPostIds = this._smallActionPostIds(); - this.set("selectedPostIds", [ - ...this.get("model.postStream.stream").filter( - (postId) => !smallActionsPostIds.has(postId) - ), - ]); - this._forceRefreshPostStream(); - }, - - deselectAll() { - this.set("selectedPostIds", []); - this._forceRefreshPostStream(); - }, - togglePostSelection(post) { const selected = this.selectedPostIds; selected.includes(post.id) @@ -973,13 +998,6 @@ export default Controller.extend(bufferedProperty("model"), { .then(() => this.updateQueryParams); }, - editTopic() { - if (this.get("model.details.can_edit")) { - this.set("editingTopic", true); - } - return false; - }, - cancelEditingTopic() { this.set("editingTopic", false); this.rollbackBuffer(); @@ -1159,10 +1177,6 @@ export default Controller.extend(bufferedProperty("model"), { .catch(popupAjaxError); }, - removeFeaturedLink() { - this.set("buffered.featured_link", null); - }, - resetBumpDate() { this.model.resetBumpDate(); }, diff --git a/app/assets/javascripts/discourse/app/controllers/user-private-messages-tags.js b/app/assets/javascripts/discourse/app/controllers/user-private-messages-tags.js index 9d4f536866..b73149358f 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-private-messages-tags.js +++ b/app/assets/javascripts/discourse/app/controllers/user-private-messages-tags.js @@ -1,25 +1,29 @@ import Controller from "@ember/controller"; +import { action } from "@ember/object"; + export default Controller.extend({ sortProperties: ["count:desc", "id"], tagsForUser: null, sortedByCount: true, sortedByName: false, - actions: { - sortByCount() { - this.setProperties({ - sortProperties: ["count:desc", "id"], - sortedByCount: true, - sortedByName: false, - }); - }, + @action + sortByCount(event) { + event?.preventDefault(); + this.setProperties({ + sortProperties: ["count:desc", "id"], + sortedByCount: true, + sortedByName: false, + }); + }, - sortById() { - this.setProperties({ - sortProperties: ["id"], - sortedByCount: false, - sortedByName: true, - }); - }, + @action + sortById(event) { + event?.preventDefault(); + this.setProperties({ + sortProperties: ["id"], + sortedByCount: false, + sortedByName: true, + }); }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/user-topics-list.js b/app/assets/javascripts/discourse/app/controllers/user-topics-list.js index 021502f615..a40ab97f4e 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-topics-list.js +++ b/app/assets/javascripts/discourse/app/controllers/user-topics-list.js @@ -81,10 +81,10 @@ export default Controller.extend(BulkTopicSelection, { }, @action - showInserted() { + showInserted(event) { + event?.preventDefault(); this.model.loadBefore(this.pmTopicTrackingState.newIncoming); this.pmTopicTrackingState.resetIncomingTracking(); - return false; }, @action diff --git a/app/assets/javascripts/discourse/app/controllers/user.js b/app/assets/javascripts/discourse/app/controllers/user.js index 4c8ba40cbc..8f2d24beb1 100644 --- a/app/assets/javascripts/discourse/app/controllers/user.js +++ b/app/assets/javascripts/discourse/app/controllers/user.js @@ -211,6 +211,15 @@ export default Controller.extend(CanCheckEmails, { } }, + @action + showSuspensions(event) { + event?.preventDefault(); + this.adminTools.showActionLogs(this, { + target_user: this.get("model.username"), + action_name: "suspend_user", + }); + }, + actions: { collapseProfile() { this.set("forceExpand", false); @@ -220,13 +229,6 @@ export default Controller.extend(CanCheckEmails, { this.set("forceExpand", true); }, - showSuspensions() { - this.adminTools.showActionLogs(this, { - target_user: this.get("model.username"), - action_name: "suspend_user", - }); - }, - adminDelete() { const userId = this.get("model.id"); const location = document.location.pathname; diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js index d2ae39cdea..9f5b02fac4 100644 --- a/app/assets/javascripts/discourse/app/lib/utilities.js +++ b/app/assets/javascripts/discourse/app/lib/utilities.js @@ -489,6 +489,12 @@ export function inCodeBlock(text, pos) { return lastOpenBlock !== -1 && pos >= end + lastOpenBlock; } +// Return an array of modifier keys that are pressed during a given `MouseEvent` +// or `KeyboardEvent`. +export function modKeysPressed(event) { + return ["alt", "shift", "meta", "ctrl"].filter((key) => event[`${key}Key`]); +} + export function translateModKey(string) { const { isApple } = helperContext().capabilities; // Apple device users are used to glyphs for shortcut keys diff --git a/app/assets/javascripts/discourse/app/templates/components/categories-only.hbs b/app/assets/javascripts/discourse/app/templates/components/categories-only.hbs index 338e4b1cf5..6f3be0d4a9 100644 --- a/app/assets/javascripts/discourse/app/templates/components/categories-only.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/categories-only.hbs @@ -20,7 +20,7 @@ {{#if this.mutedCategories}}
    - +

    {{i18n "categories.muted"}}

    {{#if this.mutedToggleIcon}} {{d-icon this.mutedToggleIcon}} diff --git a/app/assets/javascripts/discourse/app/templates/components/category-permission-row.hbs b/app/assets/javascripts/discourse/app/templates/components/category-permission-row.hbs index 16e062fda2..4acbfc6298 100644 --- a/app/assets/javascripts/discourse/app/templates/components/category-permission-row.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/category-permission-row.hbs @@ -1,6 +1,6 @@ {{this.group_name}} -
    + {{d-icon "far-trash-alt"}} diff --git a/app/assets/javascripts/discourse/app/templates/components/choose-message.hbs b/app/assets/javascripts/discourse/app/templates/components/choose-message.hbs index ce6c36f17d..c7d07e87f2 100644 --- a/app/assets/javascripts/discourse/app/templates/components/choose-message.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/choose-message.hbs @@ -11,7 +11,7 @@ {{#each this.messages as |m|}}
    @@ -237,8 +231,6 @@ {{#if this.currentUser.redesigned_user_page_nav_enabled}}
    - {{#if (or this.site.desktopView (not this.displayUserNav))}} -
    - {{outlet}} -
    - {{/if}} +
    + {{outlet}} +
    {{else}}
    diff --git a/app/assets/javascripts/discourse/app/templates/user/activity.hbs b/app/assets/javascripts/discourse/app/templates/user/activity.hbs index 18a0971b36..9d53a20d0f 100644 --- a/app/assets/javascripts/discourse/app/templates/user/activity.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/activity.hbs @@ -1,8 +1,8 @@ {{#if this.currentUser.redesigned_user_page_nav_enabled}} -
    -