diff --git a/Gemfile.lock b/Gemfile.lock index ef058bd59c..96e92f048f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -476,10 +476,10 @@ 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.1.0) + test-prof (1.2.0) thor (1.2.1) tilt (2.0.11) timeout (0.3.1) 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-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/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/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/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/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/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/ember-cli-build.js b/app/assets/javascripts/discourse/ember-cli-build.js index 57349327ea..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"); @@ -56,6 +36,9 @@ module.exports = function (defaults) { enabled: true, }, autoImport: { + alias: { + "virtual-dom": "@discourse/virtual-dom", + }, forbidEval: true, insertScriptsAt: "ember-auto-import-scripts", webpack: { @@ -108,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/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 280007c0cc..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", @@ -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/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/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/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/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` 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..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" @@ -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" 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/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; } } 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 198fe481b2..a029f1cab8 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" @@ -6012,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/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} diff --git a/plugins/chat/app/models/chat_message.rb b/plugins/chat/app/models/chat_message.rb index 39d7c0ab98..677c14aa22 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 015c081e52..0ff44ba499 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

") 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 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 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 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