From 5a94b33b3f313015bec65bb1b6b680e3462eb991 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Wed, 8 Feb 2023 17:01:52 -0800 Subject: [PATCH 01/15] DEV: Assign `TODO` to `@keegan` (#20224) --- spec/system/admin_customize_form_templates_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/system/admin_customize_form_templates_spec.rb b/spec/system/admin_customize_form_templates_spec.rb index 664448a1d3..270fb065fb 100644 --- a/spec/system/admin_customize_form_templates_spec.rb +++ b/spec/system/admin_customize_form_templates_spec.rb @@ -29,7 +29,7 @@ describe "Admin Customize Form Templates", type: :system, js: true do it "should prefill form data" do visit("/admin/customize/form-templates/#{form_template.id}") expect(form_template_page).to have_name_value(form_template.name) - # difficult to test the ace editor content (todo later) + # TODO(@keegan) difficult to test the ace editor content, todo later end end From a0ea17faeace186bba68391ea4ecfbb5615f2c98 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Wed, 8 Feb 2023 21:38:23 -0500 Subject: [PATCH 02/15] FEATURE: Add shortcut to insert current time in composer (#20216) Hitting `Ctrl+Shift+.` on Windows and `Command+Shift+.` on Mac will insert the current time in the composer. --- .../discourse/app/components/d-editor.js | 14 +++++++++ .../controllers/keyboard-shortcuts-help.js | 4 +++ .../tests/acceptance/composer-test.js | 29 +++++++++++++++++++ config/locales/client.en.yml | 1 + 4 files changed, 48 insertions(+) diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index a622955b4c..03734ae4b8 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -276,6 +276,11 @@ export default Component.extend(TextareaTextManipulation, { this._itsatrap.bind("tab", () => this.indentSelection("right")); this._itsatrap.bind("shift+tab", () => this.indentSelection("left")); + const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); + const mod = mac ? "meta" : "ctrl"; + + this._itsatrap.bind(`${mod}+shift+.`, () => this.send("insertCurrentTime")); + // disable clicking on links in the preview this.element .querySelector(".d-editor-preview") @@ -763,6 +768,15 @@ export default Component.extend(TextareaTextManipulation, { } }, + insertCurrentTime() { + const sel = this.getSelected("", { lineVal: true }); + const timezone = this.currentUser.user_option.timezone; + const time = moment().format("HH:mm:ss"); + const date = moment().format("YYYY-MM-DD"); + + this.addText(sel, `[date=${date} time=${time} timezone="${timezone}"]`); + }, + focusIn() { this.set("isEditorFocused", true); }, diff --git a/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js b/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js index 84526f584e..64462a746a 100644 --- a/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js +++ b/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js @@ -194,6 +194,10 @@ export default Controller.extend(ModalFunctionality, { keys1: [SHIFT, "F11"], keysDelimiter: PLUS, }), + insertCurrentTime: buildShortcut("composing.insert_current_time", { + keys1: [META, SHIFT, "."], + keysDelimiter: PLUS, + }), }, }, bookmarks: { diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js index e421601e62..3bcdcfe7b5 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js @@ -1360,3 +1360,32 @@ acceptance("Composer - default category not set", function (needs) { }); }); // END: Default Composer Category tests + +acceptance("Composer - current time", function (needs) { + needs.user(); + + test("composer insert current time shortcut", async function (assert) { + await visit("/t/internationalization-localization/280"); + + await click("#topic-footer-buttons .btn.create"); + assert.ok(exists(".d-editor-input"), "the composer input is visible"); + await fillIn(".d-editor-input", "and the time now is: "); + + const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); + + await triggerKeyEvent(".d-editor-input", "keydown", ".", { + shiftKey: true, + ctrlKey: !mac, + metaKey: mac, + }); + + const time = moment().format("HH:mm:ss"); + const date = moment().format("YYYY-MM-DD"); + + assert.strictEqual( + query("#reply-control .d-editor-input").value.trim(), + `and the time now is: [date=${date} time=${time} timezone="Australia/Brisbane"]`, + "it adds the current date" + ); + }); +}); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 198fe481b2..4493f88bbc 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4059,6 +4059,7 @@ en: title: "Composing" return: "%{shortcut} Return to composer" fullscreen: "%{shortcut} Fullscreen composer" + insert_current_time: "%{shortcut} Insert current time" bookmarks: title: "Bookmarking" enter: "%{shortcut} Save and close" From fc166258d34f677edb23c0be5f5acb0a88277f8d Mon Sep 17 00:00:00 2001 From: Krzysztof Kotlarek Date: Thu, 9 Feb 2023 14:20:04 +1100 Subject: [PATCH 03/15] FIX: IconPicker option to display only available icons (#20226) Not all icons are shipped by default. Sidebar section icon picker should only display available icons. --- .../templates/modal/sidebar-section-form.hbs | 1 + .../components/select-kit/icon-picker-test.js | 58 +++++++++++++++++++ .../addon/components/icon-picker.js | 8 +++ 3 files changed, 67 insertions(+) create mode 100644 app/assets/javascripts/discourse/tests/integration/components/select-kit/icon-picker-test.js diff --git a/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs b/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs index fe5ac508c5..66c864a0ca 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs @@ -26,6 +26,7 @@ @value={{link.icon}} @options={{hash maximum=1}} class={{link.iconCssClass}} + @onlyAvailable={{true}} @onChange={{action (mut link.icon)}} /> diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/icon-picker-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/icon-picker-test.js new file mode 100644 index 0000000000..1648940c9d --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/icon-picker-test.js @@ -0,0 +1,58 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import { render } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; +import selectKit from "discourse/tests/helpers/select-kit-helper"; +import { queryAll } from "discourse/tests/helpers/qunit-helpers"; +import pretender, { response } from "discourse/tests/helpers/create-pretender"; +import { setIconList } from "discourse-common/lib/icon-library"; + +module("Integration | Component | select-kit/icon-picker", function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.set("subject", selectKit()); + setIconList(["ad", "link"]); + + pretender.get("/svg-sprite/picker-search", () => { + return response([ + { id: "ad", symbol: "" }, + { id: "bacon", symbol: "" }, + { id: "link", symbol: [] }, + ]); + }); + }); + + test("content", async function (assert) { + await render(hbs` + + `); + + await this.subject.expand(); + const icons = [...queryAll(".select-kit-row .name")].map( + (el) => el.innerText + ); + assert.deepEqual(icons, ["ad", "bacon", "link"]); + }); + + test("only available", async function (assert) { + await render(hbs` + + `); + + await this.subject.expand(); + const icons = [...queryAll(".select-kit-row .name")].map( + (el) => el.innerText + ); + assert.deepEqual(icons, ["ad", "link"]); + }); +}); diff --git a/app/assets/javascripts/select-kit/addon/components/icon-picker.js b/app/assets/javascripts/select-kit/addon/components/icon-picker.js index ab6553acbd..2ce9d6f77b 100644 --- a/app/assets/javascripts/select-kit/addon/components/icon-picker.js +++ b/app/assets/javascripts/select-kit/addon/components/icon-picker.js @@ -2,6 +2,7 @@ import { convertIconClass, disableMissingIconWarning, enableMissingIconWarning, + isExistingIconId, } from "discourse-common/lib/icon-library"; import MultiSelectComponent from "select-kit/components/multi-select"; import { computed } from "@ember/object"; @@ -39,6 +40,9 @@ export default MultiSelectComponent.extend({ data: { filter }, }).then((icons) => { icons = icons.map(this._processIcon); + if (this.onlyAvailable) { + icons = icons.filter(this._iconExists); + } if (filter === "") { this._cachedIconsList = icons; } @@ -47,6 +51,10 @@ export default MultiSelectComponent.extend({ } }, + _iconExists(icon) { + return isExistingIconId(icon.id); + }, + _processIcon(icon) { const iconName = typeof icon === "object" ? icon.id : icon, strippedIconName = convertIconClass(iconName); From d1e844841da31940514f37f1d5625d60ef7af96a Mon Sep 17 00:00:00 2001 From: GeckoLinux Date: Wed, 8 Feb 2023 22:20:46 -0500 Subject: [PATCH 04/15] Fix occasional bug in order of imported comments (#20204) This bug is actually a Drupal issue where some edited posts have their `created` and `changed` timestamps set to the same value. But even when that happens in Drupal it still maintains the correct post order in an affected thread. This PR makes the Discourse importer also maintain the original Drupal comment order by sorting comments in the source DB by their `cid`, which is sequential and never changes. More details from this post onward: https://meta.discourse.org/t/large-drupal-forum-migration-importer-errors-and-limitations/246939/24?u=rahim123 --- script/import_scripts/drupal.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/script/import_scripts/drupal.rb b/script/import_scripts/drupal.rb index ac01a2daa4..15bc3d8b00 100644 --- a/script/import_scripts/drupal.rb +++ b/script/import_scripts/drupal.rb @@ -223,6 +223,7 @@ class ImportScripts::Drupal < ImportScripts::Base AND c.status = 1 AND n.type IN ('blog', 'forum') AND n.status = 1 + ORDER BY c.cid ASC LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL From f1a52db0a07051487d422eaa08f07b4ec243fd2f Mon Sep 17 00:00:00 2001 From: Krzysztof Kotlarek Date: Thu, 9 Feb 2023 15:20:34 +1100 Subject: [PATCH 05/15] Revert "FIX: IconPicker option to display only available icons (#20226)" (#20227) This reverts commit fc166258d34f677edb23c0be5f5acb0a88277f8d. --- .../templates/modal/sidebar-section-form.hbs | 1 - .../components/select-kit/icon-picker-test.js | 58 ------------------- .../addon/components/icon-picker.js | 8 --- 3 files changed, 67 deletions(-) delete mode 100644 app/assets/javascripts/discourse/tests/integration/components/select-kit/icon-picker-test.js diff --git a/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs b/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs index 66c864a0ca..fe5ac508c5 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs @@ -26,7 +26,6 @@ @value={{link.icon}} @options={{hash maximum=1}} class={{link.iconCssClass}} - @onlyAvailable={{true}} @onChange={{action (mut link.icon)}} /> diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/icon-picker-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/icon-picker-test.js deleted file mode 100644 index 1648940c9d..0000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/icon-picker-test.js +++ /dev/null @@ -1,58 +0,0 @@ -import { module, test } from "qunit"; -import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { render } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; -import selectKit from "discourse/tests/helpers/select-kit-helper"; -import { queryAll } from "discourse/tests/helpers/qunit-helpers"; -import pretender, { response } from "discourse/tests/helpers/create-pretender"; -import { setIconList } from "discourse-common/lib/icon-library"; - -module("Integration | Component | select-kit/icon-picker", function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set("subject", selectKit()); - setIconList(["ad", "link"]); - - pretender.get("/svg-sprite/picker-search", () => { - return response([ - { id: "ad", symbol: "" }, - { id: "bacon", symbol: "" }, - { id: "link", symbol: [] }, - ]); - }); - }); - - test("content", async function (assert) { - await render(hbs` - - `); - - await this.subject.expand(); - const icons = [...queryAll(".select-kit-row .name")].map( - (el) => el.innerText - ); - assert.deepEqual(icons, ["ad", "bacon", "link"]); - }); - - test("only available", async function (assert) { - await render(hbs` - - `); - - await this.subject.expand(); - const icons = [...queryAll(".select-kit-row .name")].map( - (el) => el.innerText - ); - assert.deepEqual(icons, ["ad", "link"]); - }); -}); diff --git a/app/assets/javascripts/select-kit/addon/components/icon-picker.js b/app/assets/javascripts/select-kit/addon/components/icon-picker.js index 2ce9d6f77b..ab6553acbd 100644 --- a/app/assets/javascripts/select-kit/addon/components/icon-picker.js +++ b/app/assets/javascripts/select-kit/addon/components/icon-picker.js @@ -2,7 +2,6 @@ import { convertIconClass, disableMissingIconWarning, enableMissingIconWarning, - isExistingIconId, } from "discourse-common/lib/icon-library"; import MultiSelectComponent from "select-kit/components/multi-select"; import { computed } from "@ember/object"; @@ -40,9 +39,6 @@ export default MultiSelectComponent.extend({ data: { filter }, }).then((icons) => { icons = icons.map(this._processIcon); - if (this.onlyAvailable) { - icons = icons.filter(this._iconExists); - } if (filter === "") { this._cachedIconsList = icons; } @@ -51,10 +47,6 @@ export default MultiSelectComponent.extend({ } }, - _iconExists(icon) { - return isExistingIconId(icon.id); - }, - _processIcon(icon) { const iconName = typeof icon === "object" ? icon.id : icon, strippedIconName = convertIconClass(iconName); From e216a98f74350fafe5c6b8cecbb4dcb7a30ae5d6 Mon Sep 17 00:00:00 2001 From: Aleksey Bogdanov Date: Thu, 9 Feb 2023 12:46:10 +0200 Subject: [PATCH 06/15] FIX: stop youtube autoplay on scrollups (#19951) When a user pauses a youtube video and scrolls up high enough to load new posts, the video either rewinds or restarts depending on the browser. The problem is solved by patching virtual-dom to handle element prepends without reordering old elements. Long-term, Discourse intends to move away from the vdom/widget implementation, so this is intended as a short-term solution. More context at https://meta.discourse.org/t/57692 Co-authored-by: David Taylor --- .../javascripts/discourse/ember-cli-build.js | 3 + app/assets/javascripts/discourse/package.json | 2 +- .../components/widgets/widget-test.js | 72 +++++++++++++++++++ app/assets/javascripts/yarn.lock | 28 ++++---- 4 files changed, 90 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/discourse/ember-cli-build.js b/app/assets/javascripts/discourse/ember-cli-build.js index 57349327ea..336efc808c 100644 --- a/app/assets/javascripts/discourse/ember-cli-build.js +++ b/app/assets/javascripts/discourse/ember-cli-build.js @@ -56,6 +56,9 @@ module.exports = function (defaults) { enabled: true, }, autoImport: { + alias: { + "virtual-dom": "@discourse/virtual-dom", + }, forbidEval: true, insertScriptsAt: "ember-auto-import-scripts", webpack: { diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 280007c0cc..452c08a0c4 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -94,7 +94,7 @@ "terser": "^5.16.3", "tippy.js": "^6.3.7", "util": "^0.12.5", - "virtual-dom": "^2.1.1", + "@discourse/virtual-dom": "^2.1.2-0", "webpack": "^5.75.0", "wizard": "1.0.0", "xss": "^1.0.14" diff --git a/app/assets/javascripts/discourse/tests/integration/components/widgets/widget-test.js b/app/assets/javascripts/discourse/tests/integration/components/widgets/widget-test.js index f540666270..04cf9f3195 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/widgets/widget-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/widgets/widget-test.js @@ -9,6 +9,7 @@ import { Promise } from "rsvp"; import { createWidget } from "discourse/widgets/widget"; import { next } from "@ember/runloop"; import { withPluginApi } from "discourse/lib/plugin-api"; +import { h } from "virtual-dom"; module("Integration | Component | Widget | base", function (hooks) { setupRenderingTest(hooks); @@ -394,4 +395,75 @@ module("Integration | Component | Widget | base", function (hooks) { "renders container with overridden tagName" ); }); + + test("avoids rerendering on prepend", async function (assert) { + createWidget("prepend-test", { + tagName: "div.test", + html(attrs) { + const result = []; + result.push( + this.attach("button", { + label: "rerender", + className: "rerender", + action: "dummyAction", + }) + ); + result.push( + h( + "div", + attrs.array.map((val) => h(`span.val.${val}`, { key: val }, val)) + ) + ); + return result; + }, + dummyAction() {}, + }); + + const array = ["ElementOne", "ElementTwo"]; + this.set("args", { array }); + + await render( + hbs`` + ); + + const startElements = Array.from(document.querySelectorAll("span.val")); + assert.deepEqual( + startElements.map((e) => e.innerText), + ["ElementOne", "ElementTwo"] + ); + const elementOneBefore = startElements[0]; + + const parent = elementOneBefore.parentNode; + const observer = new MutationObserver(function (mutations) { + assert.notOk( + mutations.some((m) => + Array.from(m.addedNodes).includes(elementOneBefore) + ) + ); + }); + observer.observe(parent, { childList: true }); + + array.unshift( + "PrependedElementOne", + "PrependedElementTwo", + "PrependedElementThree" + ); + + await click(".rerender"); + + const endElements = Array.from(document.querySelectorAll("span.val")); + assert.deepEqual( + endElements.map((e) => e.innerText), + [ + "PrependedElementOne", + "PrependedElementTwo", + "PrependedElementThree", + "ElementOne", + "ElementTwo", + ] + ); + const elementOneAfter = endElements[3]; + + assert.strictEqual(elementOneBefore, elementOneAfter); + }); }); diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index aebcc597cb..c83d31df79 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -1013,6 +1013,20 @@ resolved "https://registry.yarnpkg.com/@discourse/itsatrap/-/itsatrap-2.0.10.tgz#c7e750eeb32b54e769e952c4ecc472213eb1385a" integrity sha512-Jn1gdiyHMGUsmUfLFf4Q7VnTAv0l7NePbegU6pKhKHEmbzV3FosGxq30fTOYgVyTS1bxqGjlA6LvQttJpv3ROw== +"@discourse/virtual-dom@^2.1.2-0": + version "2.1.2-0" + resolved "https://registry.yarnpkg.com/@discourse/virtual-dom/-/virtual-dom-2.1.2-0.tgz#74e44261c7b0a99b3bf6db0eac37b86e978906a6" + integrity sha512-5sTfdNxyrFK9yb98YLBAChYiO2K6Go7ptErVUQciT7rgueoGyLyw6Sm0FeVkSK1GLfusYFKZG8ch2vGNzJ0wlQ== + dependencies: + browser-split "0.0.1" + error "^4.3.0" + ev-store "^7.0.0" + global "^4.3.0" + is-object "^1.0.1" + next-tick "^0.2.2" + x-is-array "0.1.0" + x-is-string "0.1.0" + "@ember-compat/tracked-built-ins@^0.9.1": version "0.9.1" resolved "https://registry.yarnpkg.com/@ember-compat/tracked-built-ins/-/tracked-built-ins-0.9.1.tgz#4cc97c1841425fbf812ef3c63c00ab4790fc32a0" @@ -9315,20 +9329,6 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -virtual-dom@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/virtual-dom/-/virtual-dom-2.1.1.tgz#80eda2d481b9ede0c049118cefcb4a05f21d1375" - integrity sha1-gO2i1IG57eDASRGM78tKBfIdE3U= - dependencies: - browser-split "0.0.1" - error "^4.3.0" - ev-store "^7.0.0" - global "^4.3.0" - is-object "^1.0.1" - next-tick "^0.2.2" - x-is-array "0.1.0" - x-is-string "0.1.0" - w3c-xmlserializer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" From 5dc28ceb1bff604f8cc284fe8eacfe0bdd98048e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 12:24:04 +0100 Subject: [PATCH 07/15] Build(deps-dev): Bump test-prof from 1.1.0 to 1.2.0 (#20223) Bumps [test-prof](https://github.com/test-prof/test-prof) from 1.1.0 to 1.2.0. - [Release notes](https://github.com/test-prof/test-prof/releases) - [Changelog](https://github.com/test-prof/test-prof/blob/master/CHANGELOG.md) - [Commits](https://github.com/test-prof/test-prof/compare/v1.1.0...v1.2.0) --- updated-dependencies: - dependency-name: test-prof dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ef058bd59c..e309142421 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -479,7 +479,7 @@ GEM syntax_tree (5.2.0) prettier_print (>= 1.2.0) syntax_tree-disable_ternary (1.0.0) - test-prof (1.1.0) + test-prof (1.2.0) thor (1.2.1) tilt (2.0.11) timeout (0.3.1) From f0322e991ffe8f315e573b42f4b02ef3607ba66d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 11:25:21 +0000 Subject: [PATCH 08/15] Build(deps-dev): Bump syntax_tree from 5.2.0 to 5.3.0 Bumps [syntax_tree](https://github.com/kddnewton/syntax_tree) from 5.2.0 to 5.3.0. - [Release notes](https://github.com/kddnewton/syntax_tree/releases) - [Changelog](https://github.com/ruby-syntax-tree/syntax_tree/blob/main/CHANGELOG.md) - [Commits](https://github.com/kddnewton/syntax_tree/compare/v5.2.0...v5.3.0) --- updated-dependencies: - dependency-name: syntax_tree dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e309142421..96e92f048f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -476,7 +476,7 @@ GEM sprockets (>= 3.0.0) sshkey (2.0.0) stackprof (0.2.23) - syntax_tree (5.2.0) + syntax_tree (5.3.0) prettier_print (>= 1.2.0) syntax_tree-disable_ternary (1.0.0) test-prof (1.2.0) From 45412206f7fe41985546bf955bbbda1d2b00af83 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 9 Feb 2023 13:14:37 +0000 Subject: [PATCH 09/15] DEV: Apply updated syntax_tree --- spec/jobs/clean_up_uploads_spec.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/spec/jobs/clean_up_uploads_spec.rb b/spec/jobs/clean_up_uploads_spec.rb index 39ba932329..a0d4befcdc 100644 --- a/spec/jobs/clean_up_uploads_spec.rb +++ b/spec/jobs/clean_up_uploads_spec.rb @@ -137,9 +137,16 @@ RSpec.describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) [ - logo_upload, logo_small_upload, digest_logo_upload, mobile_logo_upload, large_icon_upload, - opengraph_image_upload, twitter_summary_large_image_upload, favicon_upload, - apple_touch_icon_upload, system_upload, + logo_upload, + logo_small_upload, + digest_logo_upload, + mobile_logo_upload, + large_icon_upload, + opengraph_image_upload, + twitter_summary_large_image_upload, + favicon_upload, + apple_touch_icon_upload, + system_upload, ].each { |record| expect(Upload.exists?(id: record.id)).to eq(true) } fabricate_upload From 0dcfd7ddeccd438fed97c15827214a3ddd944838 Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Thu, 9 Feb 2023 17:44:04 +0400 Subject: [PATCH 10/15] =?UTF-8?q?DEV:=20correct=20a=20relationship=20?= =?UTF-8?q?=E2=80=93=20a=20chat=20message=20may=20have=20several=20mention?= =?UTF-8?q?s=20(#20219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change only makes the model reflect correctly what's already happening in the database. Note that there are no calls to chat_message.chat_mention in Core and plugins so this change should be safe. Also note, that at the moment we use the chat_mentions db table only to support notifications about mentions, but we're going to start using it for other cases. This commit is the first step in that direction. --- plugins/chat/app/models/chat_message.rb | 2 +- plugins/chat/spec/models/chat_message_spec.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/chat/app/models/chat_message.rb b/plugins/chat/app/models/chat_message.rb index 2c07386bb3..e50afe6083 100644 --- a/plugins/chat/app/models/chat_message.rb +++ b/plugins/chat/app/models/chat_message.rb @@ -22,7 +22,7 @@ class ChatMessage < ActiveRecord::Base # TODO (martin) Remove this when we drop the ChatUpload table has_many :chat_uploads, dependent: :destroy has_one :chat_webhook_event, dependent: :destroy - has_one :chat_mention, dependent: :destroy + has_many :chat_mentions, dependent: :destroy scope :in_public_channel, -> { diff --git a/plugins/chat/spec/models/chat_message_spec.rb b/plugins/chat/spec/models/chat_message_spec.rb index 5f42e77f58..3633ace563 100644 --- a/plugins/chat/spec/models/chat_message_spec.rb +++ b/plugins/chat/spec/models/chat_message_spec.rb @@ -5,6 +5,8 @@ require "rails_helper" describe ChatMessage do fab!(:message) { Fabricate(:chat_message, message: "hey friend, what's up?!") } + it { is_expected.to have_many(:chat_mentions).dependent(:destroy) } + describe ".cook" do it "does not support HTML tags" do cooked = ChatMessage.cook("

test

") From a8b145547e87c36782c621c259120e896af699e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 14:51:03 +0000 Subject: [PATCH 11/15] Build(deps): Bump @babel/standalone in /app/assets/javascripts (#20153) Bumps [@babel/standalone](https://github.com/babel/babel/tree/HEAD/packages/babel-standalone) from 7.20.14 to 7.20.15. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.20.15/packages/babel-standalone) --- updated-dependencies: - dependency-name: "@babel/standalone" dependency-type: direct:production update-type: version-update:semver-patch ... 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 452c08a0c4..a220842d65 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "@babel/core": "^7.20.12", - "@babel/standalone": "^7.20.14", + "@babel/standalone": "^7.20.15", "@discourse/backburner.js": "^2.7.1-0", "@discourse/itsatrap": "^2.0.10", "@ember-compat/tracked-built-ins": "^0.9.1", diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index c83d31df79..9f41ae128a 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -956,10 +956,10 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/standalone@^7.20.14": - version "7.20.14" - resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.20.14.tgz#26b02632680ca58da9612630b1035d11e62437a1" - integrity sha512-zxdQD6+eMQumJFPOLpOZE34JAAGrZPMXCKvHR7Mtat/l+nHDOxlit5u85HDk5WkBXmvN5PhUMeimiC95KXD9+A== +"@babel/standalone@^7.20.15": + version "7.20.15" + resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.20.15.tgz#ef82f1a9789d21d8b23f74d9fa8acecbe6ced02c" + integrity sha512-B3LmZ1NHlTb2eFEaw8rftZc730Wh9MlmsH8ubb6IjsNoIk9+SQ2aAA0nrm/1806+PftPRAACPClmKTu8PG7Tew== "@babel/template@^7.16.7", "@babel/template@^7.18.10", "@babel/template@^7.20.7": version "7.20.7" From db42917563fe274a99a6253049091f3ffa5e0ec7 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 9 Feb 2023 16:05:42 +0000 Subject: [PATCH 12/15] DEV: Resolve user_option deprecations (#20192) --- .../discourse/app/components/user-status-message.js | 2 +- .../discourse/app/controllers/preferences/interface.js | 2 -- app/assets/javascripts/discourse/app/models/user.js | 8 ++++---- .../tests/acceptance/mobile-topic-bulk-actions-test.js | 2 +- .../discourse/tests/acceptance/user-card-test.js | 2 +- .../discourse/tests/acceptance/user-status-test.js | 2 +- .../javascripts/discourse/tests/fixtures/user-fixtures.js | 2 +- .../select-kit/topic-notifications-button-test.js | 2 +- 8 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/user-status-message.js b/app/assets/javascripts/discourse/app/components/user-status-message.js index e7b7405277..18934c1490 100644 --- a/app/assets/javascripts/discourse/app/components/user-status-message.js +++ b/app/assets/javascripts/discourse/app/components/user-status-message.js @@ -13,7 +13,7 @@ export default class UserStatusMessage extends Component { } const timezone = this.currentUser - ? this.currentUser.timezone + ? this.currentUser.user_option?.timezone : moment.tz.guess(); return until(this.status.ends_at, timezone, this.currentUser?.locale); diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js index 92c3d46c03..d022aec535 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js @@ -434,8 +434,6 @@ export default Controller.extend({ }, resetSeenUserTips() { - this.model.set("skip_new_user_tips", false); - this.model.set("seen_popups", null); this.model.set("user_option.skip_new_user_tips", false); this.model.set("user_option.seen_popups", null); return this.model.save(["skip_new_user_tips", "seen_popups"]); diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js index a722e8203c..db53e68c53 100644 --- a/app/assets/javascripts/discourse/app/models/user.js +++ b/app/assets/javascripts/discourse/app/models/user.js @@ -1179,7 +1179,7 @@ const User = RestModel.extend({ showUserTip(options) { const userTips = Site.currentProp("user_tips"); - if (!userTips || this.skip_new_user_tips) { + if (!userTips || this.user_option?.skip_new_user_tips) { return; } @@ -1191,7 +1191,7 @@ const User = RestModel.extend({ return; } - const seenUserTips = this.seen_popups || []; + const seenUserTips = this.user_option?.seen_popups || []; if ( seenUserTips.includes(-1) || seenUserTips.includes(userTips[options.id]) @@ -1208,7 +1208,7 @@ const User = RestModel.extend({ hideUserTipForever(userTipId) { const userTips = Site.currentProp("user_tips"); - if (!userTips || this.skip_new_user_tips) { + if (!userTips || this.user_option?.skip_new_user_tips) { return; } @@ -1228,7 +1228,7 @@ const User = RestModel.extend({ } // Update list of seen user tips. - let seenUserTips = this.seen_popups || []; + let seenUserTips = this.user_option?.seen_popups || []; if (userTipId) { if (seenUserTips.includes(userTips[userTipId])) { return; diff --git a/app/assets/javascripts/discourse/tests/acceptance/mobile-topic-bulk-actions-test.js b/app/assets/javascripts/discourse/tests/acceptance/mobile-topic-bulk-actions-test.js index 82cb0a2118..6647ad6448 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/mobile-topic-bulk-actions-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/mobile-topic-bulk-actions-test.js @@ -23,7 +23,7 @@ acceptance("Topic - Bulk Actions - Mobile", function (needs) { }); test("bulk select - modal", async function (assert) { - updateCurrentUser({ moderator: true, enable_defer: true }); + updateCurrentUser({ moderator: true, user_option: { enable_defer: true } }); await visit("/latest"); await click("button.bulk-select"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js index 43904e65b7..d7c08f30b0 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js @@ -14,7 +14,7 @@ acceptance("User Card - Show Local Time", function (needs) { needs.settings({ display_local_time_in_user_card: true }); test("user card local time - does not update timezone for another user", async function (assert) { - User.current().timezone = "Australia/Brisbane"; + User.current().user_option.timezone = "Australia/Brisbane"; await visit("/t/internationalization-localization/280"); await click('a[data-user-card="charlie"]'); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js index 13cf56be2f..010a2cd450 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js @@ -482,7 +482,7 @@ acceptance("User Status - new user menu", function (needs) { needs.user({ id: userId, - timezone: userTimezone, + "user_option.timezone": userTimezone, redesigned_user_menu_enabled: true, }); diff --git a/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js index 979543af5d..464b039152 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js @@ -3118,8 +3118,8 @@ export default { text_size: "normal", text_size_seq: 0, title_count_mode: "notifications", + timezone: "Asia/Tokyo", }, - timezone: "Asia/Tokyo", }, }, "/u/%E3%83%A9%E3%82%A4%E3%82%AA%E3%83%B3/summary.json": { diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-button-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-button-test.js index d605269d55..452aa10459 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-button-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-button-test.js @@ -85,7 +85,7 @@ module( }); test("notification reason text - user mailing list mode", async function (assert) { - this.currentUser.set("mailing_list_mode", true); + this.currentUser.set("user_option.mailing_list_mode", true); this.set("topic", buildTopic.call(this, { level: 2 })); await render(hbs` From 25fabccd596a7a4b0e5e15752c037dbdb3e092ae Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 9 Feb 2023 16:24:24 +0000 Subject: [PATCH 13/15] DEV: Enable parallel babel processing in ember-cli (#20215) Ember CLI will automatically run babel transformations in parallel when the config is 'serializable', and can therefore be applied in multiple processes automatically. If any plugin is defined in an unserializable way, parallelisation will be disabled. Our discourse-widget-hbs transformer was causing parallelisation to be disabled. This commit fixes that, and also enables the throwUnlessParallelizable flag so that we catch this kind of issue more easily in future. This commit also refactors our deprecation silencing system into its own file, and uses a fake babel plugin to ensure deprecations are silenced in babel worker processes. In our GitHub CI jobs, this doubles the speed of ember builds (1m30s -> 45s). It should also improve production deploy times, and cold-start dev builds. --- .../javascripts/discourse-widget-hbs/index.js | 14 ++--- .../lib}/widget-hbs-compiler.js | 2 + .../javascripts/discourse/ember-cli-build.js | 36 ++++-------- .../discourse/lib/deprecation-silencer.js | 56 +++++++++++++++++++ lib/discourse_js_processor.rb | 5 +- 5 files changed, 81 insertions(+), 32 deletions(-) rename {lib/javascripts => app/assets/javascripts/discourse-widget-hbs/lib}/widget-hbs-compiler.js (99%) create mode 100644 app/assets/javascripts/discourse/lib/deprecation-silencer.js diff --git a/app/assets/javascripts/discourse-widget-hbs/index.js b/app/assets/javascripts/discourse-widget-hbs/index.js index 21fe440d27..d76f936f73 100644 --- a/app/assets/javascripts/discourse-widget-hbs/index.js +++ b/app/assets/javascripts/discourse-widget-hbs/index.js @@ -1,9 +1,6 @@ "use strict"; -const WidgetHbsCompiler = - require("../../../../lib/javascripts/widget-hbs-compiler").WidgetHbsCompiler; - -const glimmer = require("@glimmer/syntax"); +const widgetHbsCompilerPath = require.resolve("./lib/widget-hbs-compiler"); module.exports = { name: require("./package").name, @@ -15,9 +12,12 @@ module.exports = { addonOptions.babel.plugins = addonOptions.babel.plugins || []; let babelPlugins = addonOptions.babel.plugins; - WidgetHbsCompiler.cacheKey = () => "discourse-widget-hbs"; - WidgetHbsCompiler.glimmer = glimmer; - babelPlugins.push(WidgetHbsCompiler); + babelPlugins.push({ + _parallelBabel: { + requireFile: widgetHbsCompilerPath, + useMethod: "WidgetHbsCompiler", + }, + }); }, _getAddonOptions() { diff --git a/lib/javascripts/widget-hbs-compiler.js b/app/assets/javascripts/discourse-widget-hbs/lib/widget-hbs-compiler.js similarity index 99% rename from lib/javascripts/widget-hbs-compiler.js rename to app/assets/javascripts/discourse-widget-hbs/lib/widget-hbs-compiler.js index 0b70f16695..9efebf16ec 100644 --- a/lib/javascripts/widget-hbs-compiler.js +++ b/app/assets/javascripts/discourse-widget-hbs/lib/widget-hbs-compiler.js @@ -373,4 +373,6 @@ const WidgetHbsCompiler = function (babel) { }; }; +WidgetHbsCompiler.cacheKey = () => "discourse-widget-hbs"; + exports.WidgetHbsCompiler = WidgetHbsCompiler; diff --git a/app/assets/javascripts/discourse/ember-cli-build.js b/app/assets/javascripts/discourse/ember-cli-build.js index 336efc808c..e273e21cca 100644 --- a/app/assets/javascripts/discourse/ember-cli-build.js +++ b/app/assets/javascripts/discourse/ember-cli-build.js @@ -10,36 +10,16 @@ const { parsePluginClientSettings } = require("./lib/site-settings-plugin"); const discourseScss = require("./lib/discourse-scss"); const generateScriptsTree = require("./lib/scripts"); const funnel = require("broccoli-funnel"); - -const SILENCED_WARN_PREFIXES = [ - "Setting the `jquery-integration` optional feature flag", - "The Ember Classic edition has been deprecated", - "Setting the `template-only-glimmer-components` optional feature flag to `false`", - "DEPRECATION: Invoking the `` component with positional arguments is deprecated", -]; +const DeprecationSilencer = require("./lib/deprecation-silencer"); module.exports = function (defaults) { let discourseRoot = resolve("../../../.."); let vendorJs = discourseRoot + "/vendor/assets/javascripts/"; - // Silence the warnings listed in SILENCED_WARN_PREFIXES + // Silence deprecations which we are aware of - see `lib/deprecation-silencer.js` const ui = defaults.project.ui; - const oldWriteWarning = ui.writeWarnLine.bind(ui); - ui.writeWarnLine = (message, ...args) => { - if (!SILENCED_WARN_PREFIXES.some((prefix) => message.startsWith(prefix))) { - return oldWriteWarning(message, ...args); - } - }; - - // Silence warnings which go straight to console.warn (e.g. template compiler deprecations) - /* eslint-disable no-console */ - const oldConsoleWarn = console.warn.bind(console); - console.warn = (message, ...args) => { - if (!SILENCED_WARN_PREFIXES.some((prefix) => message.startsWith(prefix))) { - return oldConsoleWarn(message, ...args); - } - }; - /* eslint-enable no-console */ + DeprecationSilencer.silenceUiWarn(ui); + DeprecationSilencer.silenceConsoleWarn(); const isProduction = EmberApp.env().includes("production"); const isTest = EmberApp.env().includes("test"); @@ -111,6 +91,14 @@ module.exports = function (defaults) { ], }, + "ember-cli-babel": { + throwUnlessParallelizable: true, + }, + + babel: { + plugins: [DeprecationSilencer.generateBabelPlugin()], + }, + // We need to build tests in prod for theme tests tests: true, diff --git a/app/assets/javascripts/discourse/lib/deprecation-silencer.js b/app/assets/javascripts/discourse/lib/deprecation-silencer.js new file mode 100644 index 0000000000..e6ff2d4107 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/deprecation-silencer.js @@ -0,0 +1,56 @@ +const SILENCED_WARN_PREFIXES = [ + "Setting the `jquery-integration` optional feature flag", + "The Ember Classic edition has been deprecated", + "Setting the `template-only-glimmer-components` optional feature flag to `false`", + "DEPRECATION: Invoking the `` component with positional arguments is deprecated", +]; + +let consoleWarnSilenced = false; + +module.exports = class DeprecationSilencer { + static silenceUiWarn(ui) { + const oldWriteWarning = ui.writeWarnLine.bind(ui); + ui.writeWarnLine = (message, ...args) => { + if ( + !SILENCED_WARN_PREFIXES.some((prefix) => message.startsWith(prefix)) + ) { + return oldWriteWarning(message, ...args); + } + }; + } + + static silenceConsoleWarn() { + if (consoleWarnSilenced) { + return; + } + /* eslint-disable no-console */ + const oldConsoleWarn = console.warn.bind(console); + console.warn = (message, ...args) => { + if ( + !SILENCED_WARN_PREFIXES.some((prefix) => message.startsWith(prefix)) + ) { + return oldConsoleWarn(message, ...args); + } + }; + /* eslint-enable no-console */ + consoleWarnSilenced = true; + } + + /** + * Generates a dummy babel plugin which applies the console.warn silences in worker + * processes. Does not actually affect babel output. + */ + static generateBabelPlugin() { + return { + _parallelBabel: { + requireFile: require.resolve("./deprecation-silencer"), + buildUsing: "babelShim", + }, + }; + } + + static babelShim() { + DeprecationSilencer.silenceConsoleWarn(); + return {}; + } +}; diff --git a/lib/discourse_js_processor.rb b/lib/discourse_js_processor.rb index f0452f0f1b..d7bfb8abf4 100644 --- a/lib/discourse_js_processor.rb +++ b/lib/discourse_js_processor.rb @@ -172,7 +172,10 @@ class DiscourseJsProcessor ) # Widget HBS compiler - widget_hbs_compiler_source = File.read("#{Rails.root}/lib/javascripts/widget-hbs-compiler.js") + widget_hbs_compiler_source = + File.read( + "#{Rails.root}/app/assets/javascripts/discourse-widget-hbs/lib/widget-hbs-compiler.js", + ) widget_hbs_compiler_source = <<~JS define("widget-hbs-compiler", ["exports"], function(exports){ #{widget_hbs_compiler_source} From 58123e8089eb9a70ced1354f1cc7bdae41270737 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Thu, 9 Feb 2023 12:35:15 -0500 Subject: [PATCH 14/15] Revert "UX: flex horizontal form controls (#20098)" (#20228) This reverts commit 15b546978f1b76d2ee58ab7d3ab0b6b949ed923f. --- app/assets/stylesheets/desktop/discourse.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index 0ddf1357e2..32e1fd72a0 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -155,8 +155,6 @@ input { } .controls { - display: flex; - align-items: center; margin-left: 160px; } } From 6338287e89f9f300371cae19db2cf043ed672337 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Thu, 9 Feb 2023 11:36:27 -0800 Subject: [PATCH 15/15] UX: Easily toggle badges in admin badge list (#20225) --- .../addon/controllers/admin-badges/show.js | 16 +++- .../addon/templates/admin-badges/show.hbs | 15 ++-- .../app/components/d-toggle-switch.hbs | 20 +++++ .../app/components/d-toggle-switch.js | 15 ++++ .../components/d-toggle-switch-test.js | 66 +++++++++++++++ .../stylesheets/common/components/_index.scss | 1 + .../common/components/d-toggle-switch.scss | 82 +++++++++++++++++++ app/controllers/admin/badges_controller.rb | 2 +- config/locales/client.en.yml | 3 +- .../javascripts/discourse/lib/dummy-data.js | 2 + .../templates/styleguide/atoms/02-buttons.hbs | 10 +++ 11 files changed, 222 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/d-toggle-switch.hbs create mode 100644 app/assets/javascripts/discourse/app/components/d-toggle-switch.js create mode 100644 app/assets/javascripts/discourse/tests/integration/components/d-toggle-switch-test.js create mode 100644 app/assets/stylesheets/common/components/d-toggle-switch.scss 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 5b098387ef..a828bbe4d3 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-badges/show.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-badges/show.js @@ -1,7 +1,6 @@ import Controller, { inject as controller } from "@ember/controller"; import { observes } from "discourse-common/utils/decorators"; import I18n from "I18n"; - import { bufferedProperty } from "discourse/mixins/buffered-content"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { next } from "@ember/runloop"; @@ -25,6 +24,14 @@ export default class AdminBadgesShowController extends Controller.extend( @tracked savingStatus = ""; @tracked selectedGraphicType = null; + get badgeEnabledLabel() { + if (this.buffered.get("enabled")) { + return "admin.badges.enabled"; + } else { + return "admin.badges.disabled"; + } + } + get badgeTypes() { return this.adminBadges.badgeTypes; } @@ -238,4 +245,11 @@ export default class AdminBadgesShowController extends Controller.extend( }, }); } + + @action + toggleBadge() { + this.model + .save({ enabled: !this.buffered.get("enabled") }) + .catch(popupAjaxError); + } } 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 4f6619b25e..d73d41dad9 100644 --- a/app/assets/javascripts/admin/addon/templates/admin-badges/show.hbs +++ b/app/assets/javascripts/admin/addon/templates/admin-badges/show.hbs @@ -1,4 +1,12 @@ +
+ +
+
@@ -253,13 +261,6 @@ {{i18n "admin.badges.show_posts"}}
- -
- -
diff --git a/app/assets/javascripts/discourse/app/components/d-toggle-switch.hbs b/app/assets/javascripts/discourse/app/components/d-toggle-switch.hbs new file mode 100644 index 0000000000..837d2ac205 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/d-toggle-switch.hbs @@ -0,0 +1,20 @@ +
+ + + {{this.computedLabel}} + +
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/d-toggle-switch.js b/app/assets/javascripts/discourse/app/components/d-toggle-switch.js new file mode 100644 index 0000000000..5dcd2cebb3 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/d-toggle-switch.js @@ -0,0 +1,15 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import I18n from "I18n"; + +export default class DiscourseToggleSwitch extends Component { + @tracked iconEnabled = true; + @tracked showIcon = this.iconEnabled && this.icon; + + get computedLabel() { + if (this.args.label) { + return I18n.t(this.args.label); + } + return this.args.translatedLabel; + } +} diff --git a/app/assets/javascripts/discourse/tests/integration/components/d-toggle-switch-test.js b/app/assets/javascripts/discourse/tests/integration/components/d-toggle-switch-test.js new file mode 100644 index 0000000000..cd7e82d736 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/d-toggle-switch-test.js @@ -0,0 +1,66 @@ +import { module, test } from "qunit"; +import { render } from "@ember/test-helpers"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import { hbs } from "ember-cli-htmlbars"; +import { exists, query } from "discourse/tests/helpers/qunit-helpers"; +import I18n from "I18n"; + +module("Integration | Component | d-toggle-switch", function (hooks) { + setupRenderingTest(hooks); + + test("it renders a toggle button in a disabled state", async function (assert) { + this.set("state", false); + + await render(hbs``); + + assert.ok(exists(".d-toggle-switch"), "it renders a toggle switch"); + assert.strictEqual( + query(".d-toggle-switch__checkbox").getAttribute("aria-checked"), + "false" + ); + }); + + test("it renders a toggle button in a enabled state", async function (assert) { + this.set("state", true); + + await render(hbs``); + + assert.ok(exists(".d-toggle-switch"), "it renders a toggle switch"); + assert.strictEqual( + query(".d-toggle-switch__checkbox").getAttribute("aria-checked"), + "true" + ); + }); + + test("it renders a checkmark icon when enabled", async function (assert) { + this.set("state", true); + + await render(hbs``); + assert.ok(exists(".d-toggle-switch__checkbox-slider .d-icon-check")); + }); + + test("it renders a label for the button", async function (assert) { + I18n.translations[I18n.locale].js.test = { fooLabel: "foo" }; + this.set("state", true); + await render( + hbs`` + ); + + this.set("label", "test.fooLabel"); + + assert.strictEqual( + query(".d-toggle-switch__checkbox-label").innerText, + I18n.t("test.fooLabel") + ); + + this.setProperties({ + label: null, + translatedLabel: "bar", + }); + + assert.strictEqual( + query(".d-toggle-switch__checkbox-label").innerText, + "bar" + ); + }); +}); diff --git a/app/assets/stylesheets/common/components/_index.scss b/app/assets/stylesheets/common/components/_index.scss index 361546e053..b33546edc1 100644 --- a/app/assets/stylesheets/common/components/_index.scss +++ b/app/assets/stylesheets/common/components/_index.scss @@ -7,6 +7,7 @@ @import "conditional-loading-section"; @import "convert-to-public-topic-modal"; @import "d-tooltip"; +@import "d-toggle-switch"; @import "date-input"; @import "date-picker"; @import "date-time-input-range"; diff --git a/app/assets/stylesheets/common/components/d-toggle-switch.scss b/app/assets/stylesheets/common/components/d-toggle-switch.scss new file mode 100644 index 0000000000..8145a45240 --- /dev/null +++ b/app/assets/stylesheets/common/components/d-toggle-switch.scss @@ -0,0 +1,82 @@ +.d-toggle-switch { + --toggle-switch-width: 45px; + --toggle-switch-height: 24px; + + &:focus { + .d-toggle-switch__checkbox-slider { + outline: 2px solid var(--tertiary); + } + } + + &:hover { + .d-toggle-switch__checkbox-slider { + background-color: var(--primary-high); + } + + .d-toggle-switch__checkbox[aria-checked="true"] + + .d-toggle-switch__checkbox-slider { + background-color: var(--tertiary-hover); + } + } + + display: flex; + align-items: center; + + &__label { + position: relative; + display: inline-block; + cursor: pointer; + } + + &__checkbox { + position: absolute; + visibility: hidden; + } + + &__checkbox[aria-checked="true"] + .d-toggle-switch__checkbox-slider { + background-color: var(--tertiary); + } + + &__checkbox[aria-checked="true"] + .d-toggle-switch__checkbox-slider::before { + left: calc(var(--toggle-switch-width) - 22px); + } + + &__checkbox-slider { + display: inline-block; + cursor: pointer; + background: var(--primary-low-mid); + border-radius: 16px; + width: var(--toggle-switch-width); + height: var(--toggle-switch-height); + margin-right: 0.5em; + position: relative; + vertical-align: middle; + transition: background 0.25s; + + .d-icon { + font-size: var(--font-down-1); + color: var(--secondary); + left: 7px; + top: 7px; + position: absolute; + } + } + + &__checkbox-slider::before, + &__checkbox-slider::after { + content: ""; + display: block; + position: absolute; + cursor: pointer; + } + + &__checkbox-slider::before { + background: var(--secondary); + border-radius: 50%; + width: calc(var(--toggle-switch-width) / 2.5); + height: calc(var(--toggle-switch-width) / 2.5); + top: 3.5px; + left: 4px; + transition: left 0.25s; + } +} diff --git a/app/controllers/admin/badges_controller.rb b/app/controllers/admin/badges_controller.rb index 4efb86cff8..4723622c25 100644 --- a/app/controllers/admin/badges_controller.rb +++ b/app/controllers/admin/badges_controller.rb @@ -15,7 +15,7 @@ class Admin::BadgesController < Admin::AdminController .includes(:badge_grouping) .includes(:badge_type, :image_upload) .references(:badge_grouping) - .order("badge_groupings.position, badge_type_id, badges.name") + .order("enabled DESC", "badge_groupings.position, badge_type_id, badges.name") .to_a, protected_system_fields: Badge.protected_system_fields, triggers: Badge.trigger_hash, diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4493f88bbc..a029f1cab8 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -6013,7 +6013,8 @@ en: allow_title: Allow badge to be used as a title multiple_grant: Can be granted multiple times listable: Show badge on the public badges page - enabled: Enable badge + enabled: enabled + disabled: disabled icon: Icon image: Image graphic: Graphic diff --git a/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js b/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js index 29597f9e57..1416d0b795 100644 --- a/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js +++ b/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js @@ -217,6 +217,8 @@ export function createData(store) { { disabled: true, text: "disabled" }, ], + toggleSwitchState: true, + navItems: ["latest", "categories", "top"].map((name) => { let item = NavItem.fromText(name); diff --git a/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/02-buttons.hbs b/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/02-buttons.hbs index bfb9f2fbe9..bca429d5d0 100644 --- a/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/02-buttons.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/02-buttons.hbs @@ -152,4 +152,14 @@ @translatedLabel={{bs.text}} /> {{/each}} + + + + \ No newline at end of file