diff --git a/Gemfile.lock b/Gemfile.lock
index c7ebc2cc8196f..7ae098ef89d45 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -233,7 +233,7 @@ GEM
reline (>= 0.4.2)
iso8601 (0.13.0)
jmespath (1.6.2)
- json (2.13.1)
+ json (2.13.2)
json-schema (5.2.1)
addressable (~> 2.8)
bigdecimal (~> 3.1)
@@ -286,7 +286,7 @@ GEM
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
- mime-types-data (3.2025.0722)
+ mime-types-data (3.2025.0729)
mini_mime (1.1.5)
mini_racer (0.19.0)
libv8-node (~> 24.1.0.0)
@@ -991,7 +991,7 @@ CHECKSUMS
irb (1.15.2) sha256=222f32952e278da34b58ffe45e8634bf4afc2dc7aa9da23fed67e581aa50fdba
iso8601 (0.13.0) sha256=298c2b15b7be5fa95a1372813d36a2257656cd8e906dfbc1f5cb409851425aa2
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
- json (2.13.1) sha256=8e7cf4bb44a789ac5fc62869c7187986251c3172d9bdfa60b92c63d6ecab0ffc
+ json (2.13.2) sha256=02e1f118d434c6b230a64ffa5c8dee07e3ec96244335c392eaed39e1199dbb68
json-schema (5.2.1) sha256=1ef39286c4771e7a71661d955fec9b66d1d1708547a0071c130af7c0a9264898
json_schemer (2.4.0) sha256=56cb6117bb5748d925b33ad3f415b513d41d25d0bbf57fe63c0a78ff05597c24
jwt (2.10.1) sha256=e6424ae1d813f63e761a04d6284e10e7ec531d6f701917fadcd0d9b2deaf1cc5
@@ -1021,7 +1021,7 @@ CHECKSUMS
messageformat-wrapper (1.1.0) sha256=ecea879626e412d1bc841c457dacfcbb1a62cf88ca83573e4ea34bb371f160bc
method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
- mime-types-data (3.2025.0722) sha256=f9d1fd57ecc5688a66d9811d45981ee58e2dca012e352a1eaa7299e8c0f482f4
+ mime-types-data (3.2025.0729) sha256=8d7e1ab1ab756ebba91354ff4e35bcf23c39ed86dc5abba6cf32ce66ee9e5aad
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
mini_racer (0.19.0) sha256=9152694738db8b145ced843126e2a391f908732c069cb97bbc909c2bac16bbc1
mini_scheduler (0.18.0) sha256=d2f084f38da8d76c5844a92f0d6bd01fc9982a8b5e6c7679b6cf44c82da33503
diff --git a/app/assets/javascripts/admin/addon/components/admin-plugins-list-item.gjs b/app/assets/javascripts/admin/addon/components/admin-plugins-list-item.gjs
index d394ad1a6253a..1c4cc3d10c024 100644
--- a/app/assets/javascripts/admin/addon/components/admin-plugins-list-item.gjs
+++ b/app/assets/javascripts/admin/addon/components/admin-plugins-list-item.gjs
@@ -121,14 +121,9 @@ export default class AdminPluginsListItem extends Component {
>
{{@plugin.version}}
{{#if this.isPreinstalled}}
-
+
{{i18n "admin.plugins.preinstalled"}}
-
+
{{else}}
{{/if}}
diff --git a/app/assets/javascripts/admin/addon/models/report.js b/app/assets/javascripts/admin/addon/models/report.js
index 3e79bfc7673f9..367b2fc7baa03 100644
--- a/app/assets/javascripts/admin/addon/models/report.js
+++ b/app/assets/javascripts/admin/addon/models/report.js
@@ -622,7 +622,7 @@ export default class Report extends EmberObject {
_percentLabel(value) {
return {
value: toNumber(value),
- formattedValue: value ? `${value}%` : "—",
+ formattedValue: value === null || value === undefined ? "—" : `${value}%`,
};
}
diff --git a/app/assets/javascripts/admin/addon/templates/plugins-index.gjs b/app/assets/javascripts/admin/addon/templates/plugins-index.gjs
index b3862ce657451..7138a2a854ed1 100644
--- a/app/assets/javascripts/admin/addon/templates/plugins-index.gjs
+++ b/app/assets/javascripts/admin/addon/templates/plugins-index.gjs
@@ -1,9 +1,10 @@
+import { concat } from "@ember/helper";
+import { htmlSafe } from "@ember/template";
import RouteTemplate from "ember-route-template";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DPageHeader from "discourse/components/d-page-header";
import NavItem from "discourse/components/nav-item";
import PluginOutlet from "discourse/components/plugin-outlet";
-import icon from "discourse/helpers/d-icon";
import lazyHash from "discourse/helpers/lazy-hash";
import { i18n } from "discourse-i18n";
import AdminFilterControls from "admin/components/admin-filter-controls";
@@ -15,8 +16,14 @@ export default RouteTemplate(
'
+ (i18n "admin.plugins.howto")
+ ""
+ )
+ }}
>
<:breadcrumbs>
@@ -48,13 +55,6 @@ export default RouteTemplate(
-
-
{{#if @controller.model.length}}
{{#if this.canCreateTopic}}
@@ -16,11 +26,20 @@ export default class CreateTopicButton extends Component {
+ <:tooltip>
+ {{#if @disabled}}
+
+ {{/if}}
+
{{#if @showDrafts}}
diff --git a/app/assets/javascripts/discourse/app/components/d-autocomplete-results.gjs b/app/assets/javascripts/discourse/app/components/d-autocomplete-results.gjs
new file mode 100644
index 0000000000000..75c47ec588a93
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/d-autocomplete-results.gjs
@@ -0,0 +1,151 @@
+import Component from "@glimmer/component";
+import { on } from "@ember/modifier";
+import { action } from "@ember/object";
+import didInsert from "@ember/render-modifiers/modifiers/did-insert";
+import didUpdate from "@ember/render-modifiers/modifiers/did-update";
+import { htmlSafe } from "@ember/template";
+
+// CSS selectors for autocomplete result items
+const RESULT_ITEM_SELECTOR = "li a";
+const SELECTED_RESULT_SELECTOR = "li a.selected";
+const SELECTED_CLASS = "selected";
+
+/**
+ * Component for rendering autocomplete results in a d-menu
+ *
+ * @component DAutocompleteResults
+ * @param {Array} data.results - Array of autocomplete results
+ * @param {number} data.selectedIndex - Currently selected index
+ * @param {Function} data.onSelect - Callback for item selection
+ * @param {Function} data.template - Template function for rendering
+ */
+export default class DAutocompleteResults extends Component {
+ isInitialRender = true;
+
+ get results() {
+ return this.args.data.getResults?.() || [];
+ }
+
+ get selectedIndex() {
+ return this.args.data.getSelectedIndex?.() || 0;
+ }
+
+ _applySelectedClass(wrapperElement, selectedIndex) {
+ const links = wrapperElement.querySelectorAll(RESULT_ITEM_SELECTOR);
+
+ // Always remove existing selected classes first
+ const selectedElements = wrapperElement.querySelectorAll(
+ SELECTED_RESULT_SELECTOR
+ );
+ selectedElements.forEach((element) =>
+ element.classList.remove(SELECTED_CLASS)
+ );
+
+ if (selectedIndex >= 0 && links[selectedIndex]) {
+ links[selectedIndex].classList.add(SELECTED_CLASS);
+ }
+
+ return links;
+ }
+
+ scrollToSelected(wrapperElement) {
+ // This is a more imperative approach that's meant to be compatible with the pre-existing autocomplete templates,
+ // we should refactor in future to use component templates that are more declarative in setting the `selected` class.
+
+ if (!wrapperElement) {
+ return;
+ }
+ // Find all links in the autocomplete menu and update selection
+ const links = this._applySelectedClass(wrapperElement, this.selectedIndex);
+
+ if (!links || links.length === 0 || !links[this.selectedIndex]) {
+ return;
+ }
+
+ links[this.selectedIndex].scrollIntoView({
+ block: "nearest",
+ behavior: "smooth",
+ });
+ }
+
+ @action
+ handleInitialRender() {
+ this.args.data.onRender?.(this.results);
+ }
+
+ @action
+ handleClick(event) {
+ if (!this.args.data.template) {
+ return;
+ }
+
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const clickedLink = event.target.closest(RESULT_ITEM_SELECTOR);
+ if (!clickedLink) {
+ return;
+ }
+
+ // Find the index of the clicked link
+ const links = event.currentTarget.querySelectorAll(RESULT_ITEM_SELECTOR);
+ const index = Array.from(links).indexOf(clickedLink);
+
+ if (index >= 0) {
+ // Call onSelect and handle any promise returned
+ const result = this.args.data.onSelect(
+ this.results[index],
+ index,
+ event
+ );
+ if (result && typeof result.then === "function") {
+ result.catch((e) => {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] onSelect promise rejected: ", e);
+ });
+ }
+ }
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] Click handler error: ", e);
+ }
+ }
+
+ @action
+ handleUpdate(wrapperElement) {
+ this.isInitialRender = false;
+ this.scrollToSelected(wrapperElement);
+ // Call onRender callback after DOM is ready
+ this.args.data.onRender?.(this.results);
+ }
+
+ get templateHTML() {
+ if (!this.args.data.template) {
+ return "";
+ }
+
+ const template = this.args.data.template({ options: this.results });
+
+ if (!this.isInitialRender || this.selectedIndex < 0) {
+ return htmlSafe(template);
+ }
+
+ const tempDiv = document.createElement("div");
+ tempDiv.innerHTML = template;
+ this._applySelectedClass(tempDiv, this.selectedIndex);
+
+ return htmlSafe(tempDiv.innerHTML);
+ }
+
+
+
+ {{this.templateHTML}}
+
+
+}
diff --git a/app/assets/javascripts/discourse/app/components/d-editor.gjs b/app/assets/javascripts/discourse/app/components/d-editor.gjs
index 03f21e6256b6d..1012a034abe10 100644
--- a/app/assets/javascripts/discourse/app/components/d-editor.gjs
+++ b/app/assets/javascripts/discourse/app/components/d-editor.gjs
@@ -22,7 +22,6 @@ import EmojiPickerDetached from "discourse/components/emoji-picker/detached";
import UpsertHyperlink from "discourse/components/modal/upsert-hyperlink";
import PluginOutlet from "discourse/components/plugin-outlet";
import PopupInputTip from "discourse/components/popup-input-tip";
-import { SKIP } from "discourse/lib/autocomplete";
import renderEmojiAutocomplete from "discourse/lib/autocomplete/emoji";
import userAutocomplete from "discourse/lib/autocomplete/user";
import Toolbar from "discourse/lib/composer/toolbar";
@@ -44,6 +43,7 @@ import {
initUserStatusHtml,
renderUserStatusHtml,
} from "discourse/lib/user-status-on-autocomplete";
+import { SKIP } from "discourse/modifiers/d-autocomplete";
import { i18n } from "discourse-i18n";
let _createCallbacks = [];
@@ -94,9 +94,9 @@ export default class DEditor extends Component {
this.setupToolbar();
- // TODO (martin) Remove this once we are sure all users have migrated
- // to the new rich editor preference, or a few months after the 3.5 release.
if (this.siteSettings.rich_editor) {
+ // TODO (martin) Remove this once we are sure all users have migrated
+ // to the new rich editor preference, or a few months after the 3.5 release.
await this.handleOldRichEditorPreference();
if (this.currentUser.useRichEditor) {
diff --git a/app/assets/javascripts/discourse/app/components/d-navigation.gjs b/app/assets/javascripts/discourse/app/components/d-navigation.gjs
index 1689bfa6b1df7..632379031a1ca 100644
--- a/app/assets/javascripts/discourse/app/components/d-navigation.gjs
+++ b/app/assets/javascripts/discourse/app/components/d-navigation.gjs
@@ -4,6 +4,7 @@ import { hash } from "@ember/helper";
import { action } from "@ember/object";
import { dependentKeyCompat } from "@ember/object/compat";
import { service } from "@ember/service";
+import { htmlSafe } from "@ember/template";
import { tagName } from "@ember-decorators/component";
import { and, gt } from "truth-helpers";
import BreadCrumbs from "discourse/components/bread-crumbs";
@@ -116,6 +117,35 @@ export default class DNavigation extends Component {
}
}
+ @discourseComputed(
+ "createTopicDisabled",
+ "categoryReadOnlyBanner",
+ "canCreateTopicOnTag",
+ "tag.id"
+ )
+ createTopicButtonDisabled(
+ createTopicDisabled,
+ categoryReadOnlyBanner,
+ canCreateTopicOnTag,
+ tagId
+ ) {
+ if (tagId && !canCreateTopicOnTag) {
+ return true;
+ } else if (categoryReadOnlyBanner) {
+ return false;
+ }
+ return createTopicDisabled;
+ }
+
+ @discourseComputed("categoryReadOnlyBanner")
+ createTopicClass(categoryReadOnlyBanner) {
+ let classNames = ["btn-default"];
+ if (categoryReadOnlyBanner) {
+ classNames.push("disabled");
+ }
+ return classNames.join(" ");
+ }
+
@discourseComputed("category.can_edit")
showCategoryEdit(canEdit) {
return canEdit;
@@ -195,7 +225,11 @@ export default class DNavigation extends Component {
@action
clickCreateTopicButton() {
- this.createTopic();
+ if (this.categoryReadOnlyBanner) {
+ this.dialog.alert({ message: htmlSafe(this.categoryReadOnlyBanner) });
+ } else {
+ this.createTopic();
+ }
}
@@ -299,7 +333,10 @@ export default class DNavigation extends Component {
diff --git a/app/assets/javascripts/discourse/app/components/edit-category-localizations.gjs b/app/assets/javascripts/discourse/app/components/edit-category-localizations.gjs
index f01f5b7f2cde9..c3075d06b6730 100644
--- a/app/assets/javascripts/discourse/app/components/edit-category-localizations.gjs
+++ b/app/assets/javascripts/discourse/app/components/edit-category-localizations.gjs
@@ -8,9 +8,15 @@ export default class EditCategoryLocalizations extends buildCategoryPanel(
"localizations"
) {
@service siteSettings;
+ @service languageNameLookup;
get availableLocales() {
- return this.siteSettings.available_content_localization_locales;
+ return this.siteSettings.available_content_localization_locales.map(
+ ({ value }) => ({
+ name: this.languageNameLookup.getLanguageName(value),
+ value,
+ })
+ );
}
diff --git a/app/assets/javascripts/discourse/app/components/modal/upsert-hyperlink.gjs b/app/assets/javascripts/discourse/app/components/modal/upsert-hyperlink.gjs
index fb1341fe4fd63..56569086e97ca 100644
--- a/app/assets/javascripts/discourse/app/components/modal/upsert-hyperlink.gjs
+++ b/app/assets/javascripts/discourse/app/components/modal/upsert-hyperlink.gjs
@@ -109,8 +109,6 @@ export default class UpsertHyperlink extends Component {
".internal-link-results .search-link"
)[this.selectedRow];
this.selectLink(selected);
- event.preventDefault();
- event.stopPropagation();
}
// this would ideally be handled by nesting a submit button within the form tag
@@ -119,6 +117,9 @@ export default class UpsertHyperlink extends Component {
this.formApi.submit();
}
+ event.preventDefault();
+ event.stopPropagation();
+
break;
case "Escape":
// Esc should cancel dropdown first
@@ -151,16 +152,8 @@ export default class UpsertHyperlink extends Component {
return;
}
- const linkText = data.linkText || "";
-
- if (linkText.length) {
- this.args.model.toolbarEvent.addText(`[${linkText}](${linkUrl})`);
- } else if (sel.value) {
- this.args.model.toolbarEvent.addText(`[${sel.value}](${linkUrl})`);
- } else {
- this.args.model.toolbarEvent.addText(`[${origLink}](${linkUrl})`);
- this.args.model.toolbarEvent.selectText(sel.start + 1, origLink.length);
- }
+ const linkText = data.linkText || sel.value || origLink || "";
+ this.args.model.toolbarEvent.addText(`[${linkText.trim()}](${linkUrl})`);
this.args.closeModal();
}
diff --git a/app/assets/javascripts/discourse/app/components/post-translation-editor.gjs b/app/assets/javascripts/discourse/app/components/post-translation-editor.gjs
index 97d3a6e447055..87fbaba2e0a71 100644
--- a/app/assets/javascripts/discourse/app/components/post-translation-editor.gjs
+++ b/app/assets/javascripts/discourse/app/components/post-translation-editor.gjs
@@ -13,6 +13,7 @@ import DropdownSelectBox from "select-kit/components/dropdown-select-box";
export default class PostTranslationEditor extends Component {
@service composer;
@service siteSettings;
+ @service languageNameLookup;
constructor() {
super(...arguments);
@@ -55,14 +56,10 @@ export default class PostTranslationEditor extends Component {
return this.siteSettings.available_content_localization_locales
.filter(({ value }) => value !== originalPostLocale)
- .map(({ native_name, name, value }) => {
- name =
- i18n(name) === native_name
- ? native_name
- : `${i18n(name)} (${native_name})`;
-
- return { name, value };
- });
+ .map(({ value }) => ({
+ name: this.languageNameLookup.getLanguageName(value),
+ value,
+ }));
}
@action
diff --git a/app/assets/javascripts/discourse/app/components/post/quoted-content.gjs b/app/assets/javascripts/discourse/app/components/post/quoted-content.gjs
index da649b0051efd..f83c9fccc187f 100644
--- a/app/assets/javascripts/discourse/app/components/post/quoted-content.gjs
+++ b/app/assets/javascripts/discourse/app/components/post/quoted-content.gjs
@@ -95,11 +95,7 @@ export default class PostQuotedContent extends Component {
}
get shouldDisplayNavigateToPostButton() {
- return (
- !this.args.quotedPostNotFound &&
- this.quotedPostUrl &&
- !this.isQuotedPostIgnored
- );
+ return !this.args.quotedPostNotFound && this.quotedPostUrl;
}
get shouldDisplayQuoteControls() {
diff --git a/app/assets/javascripts/discourse/app/components/search-menu.gjs b/app/assets/javascripts/discourse/app/components/search-menu.gjs
index b2c652630c7f9..d7b7b0155572a 100644
--- a/app/assets/javascripts/discourse/app/components/search-menu.gjs
+++ b/app/assets/javascripts/discourse/app/components/search-menu.gjs
@@ -18,7 +18,6 @@ import concatClass from "discourse/helpers/concat-class";
import lazyHash from "discourse/helpers/lazy-hash";
import loadingSpinner from "discourse/helpers/loading-spinner";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
import discourseDebounce from "discourse/lib/debounce";
import { bind } from "discourse/lib/decorators";
@@ -30,6 +29,7 @@ import {
} from "discourse/lib/search";
import DiscourseURL from "discourse/lib/url";
import userSearch from "discourse/lib/user-search";
+import { CANCELLED_STATUS } from "discourse/modifiers/d-autocomplete";
const CATEGORY_SLUG_REGEXP = /(\#[a-zA-Z0-9\-:]*)$/gi;
const USERNAME_REGEXP = /(\@[a-zA-Z0-9\-\_]*)$/gi;
diff --git a/app/assets/javascripts/discourse/app/lib/category-tag-search.js b/app/assets/javascripts/discourse/app/lib/category-tag-search.js
index 43a8ffabc37ba..29e94149fb4fb 100644
--- a/app/assets/javascripts/discourse/app/lib/category-tag-search.js
+++ b/app/assets/javascripts/discourse/app/lib/category-tag-search.js
@@ -1,13 +1,13 @@
import { cancel } from "@ember/runloop";
import { Promise } from "rsvp";
import { ajax } from "discourse/lib/ajax";
-import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
import { SEPARATOR } from "discourse/lib/category-hashtags";
import discourseDebounce from "discourse/lib/debounce";
import { isTesting } from "discourse/lib/environment";
import discourseLater from "discourse/lib/later";
import { TAG_HASHTAG_POSTFIX } from "discourse/lib/tag-hashtags";
import Category from "discourse/models/category";
+import { CANCELLED_STATUS } from "discourse/modifiers/d-autocomplete";
let cache = {};
let cacheTime;
diff --git a/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js b/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js
index 950a5e73818ab..3f8dfdd755b6c 100644
--- a/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js
+++ b/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js
@@ -1,13 +1,13 @@
import { cancel } from "@ember/runloop";
import { htmlSafe } from "@ember/template";
import { ajax } from "discourse/lib/ajax";
-import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
import discourseDebounce from "discourse/lib/debounce";
import { INPUT_DELAY, isTesting } from "discourse/lib/environment";
import { getHashtagTypeClasses as getHashtagTypeClassesNew } from "discourse/lib/hashtag-type-registry";
import discourseLater from "discourse/lib/later";
import { emojiUnescape } from "discourse/lib/text";
import { escapeExpression } from "discourse/lib/utilities";
+import { CANCELLED_STATUS } from "discourse/modifiers/d-autocomplete";
/**
* Sets up a textarea using the jQuery autocomplete plugin, specifically
diff --git a/app/assets/javascripts/discourse/app/lib/textarea-text-manipulation.js b/app/assets/javascripts/discourse/app/lib/textarea-text-manipulation.js
index 24c0d82ed0902..4bf58fad654ea 100644
--- a/app/assets/javascripts/discourse/app/lib/textarea-text-manipulation.js
+++ b/app/assets/javascripts/discourse/app/lib/textarea-text-manipulation.js
@@ -1,5 +1,5 @@
// @ts-check
-import { setOwner } from "@ember/owner";
+import { getOwner, setOwner } from "@ember/owner";
import { next, schedule } from "@ember/runloop";
import { service } from "@ember/service";
import { isEmpty } from "@ember/utils";
@@ -18,6 +18,7 @@ import {
inCodeBlock,
setCaretPosition,
} from "discourse/lib/utilities";
+import DAutocompleteModifier from "discourse/modifiers/d-autocomplete";
import { i18n } from "discourse-i18n";
/**
@@ -900,12 +901,21 @@ export default class TextareaTextManipulation {
}
autocomplete(options) {
- // @ts-ignore
- this.$textarea.autocomplete(
- options instanceof Object
- ? { textHandler: this.autocompleteHandler, ...options }
- : options
- );
+ if (this.siteSettings.floatkit_autocomplete_composer) {
+ return DAutocompleteModifier.setupAutocomplete(
+ getOwner(this),
+ this.textarea,
+ this.autocompleteHandler,
+ options
+ );
+ } else {
+ // @ts-ignore
+ this.$textarea.autocomplete(
+ options instanceof Object
+ ? { textHandler: this.autocompleteHandler, ...options }
+ : options
+ );
+ }
}
}
diff --git a/app/assets/javascripts/discourse/app/lib/user-search.js b/app/assets/javascripts/discourse/app/lib/user-search.js
index 61532620b220e..26405ba547d0d 100644
--- a/app/assets/javascripts/discourse/app/lib/user-search.js
+++ b/app/assets/javascripts/discourse/app/lib/user-search.js
@@ -1,13 +1,13 @@
import { cancel } from "@ember/runloop";
import { Promise } from "rsvp";
import { ajax } from "discourse/lib/ajax";
-import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
import { camelCaseToSnakeCase } from "discourse/lib/case-converter";
import discourseDebounce from "discourse/lib/debounce";
import { isTesting } from "discourse/lib/environment";
import discourseLater from "discourse/lib/later";
import { userPath } from "discourse/lib/url";
import { emailValid } from "discourse/lib/utilities";
+import { CANCELLED_STATUS } from "discourse/modifiers/d-autocomplete";
let cache = {},
cacheKey,
diff --git a/app/assets/javascripts/discourse/app/lib/virtual-element-from-caret-coords.js b/app/assets/javascripts/discourse/app/lib/virtual-element-from-caret-coords.js
new file mode 100644
index 0000000000000..eb0b76c9e68f9
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/virtual-element-from-caret-coords.js
@@ -0,0 +1,45 @@
+class VirtualElementFromCaretCoords {
+ constructor(caretCoords, offset = [0, 0]) {
+ this.caretCoords = caretCoords;
+ this.offset = offset;
+ this.updateRect();
+ }
+
+ updateRect() {
+ const [xOffset, yOffset] = this.offset;
+ this.rect = {
+ top: this.caretCoords.y + yOffset,
+ right: this.caretCoords.x,
+ bottom: this.caretCoords.y + yOffset,
+ left: this.caretCoords.x + xOffset,
+ width: 0,
+ height: 0,
+ x: this.caretCoords.x,
+ y: this.caretCoords.y,
+ toJSON() {
+ return this;
+ },
+ };
+ return this.rect;
+ }
+
+ getBoundingClientRect() {
+ return this.rect;
+ }
+
+ getClientRects() {
+ return [this.rect];
+ }
+
+ get clientWidth() {
+ return this.rect.width;
+ }
+
+ get clientHeight() {
+ return this.rect.height;
+ }
+}
+
+export default function virtualElementFromCaretCoords(caretCoords, offset) {
+ return new VirtualElementFromCaretCoords(caretCoords, offset);
+}
diff --git a/app/assets/javascripts/discourse/app/models/post-stream.js b/app/assets/javascripts/discourse/app/models/post-stream.js
index e6e1febad153d..7532d8a99b94a 100644
--- a/app/assets/javascripts/discourse/app/models/post-stream.js
+++ b/app/assets/javascripts/discourse/app/models/post-stream.js
@@ -843,27 +843,42 @@ export default class PostStream extends RestModel {
return Promise.resolve();
}
- triggerChangedPost(postId, updatedAt, opts) {
- opts = opts || {};
+ /**
+ * Updates a post in the stream when it has been changed on the server.
+ *
+ * @param {number} postId - The ID of the post to update
+ * @param {string} updatedAt - The timestamp when the post was last updated
+ * @param {Object} opts - Additional options for updating the post
+ * @param {boolean} [opts.preserveCooked] - Whether to preserve the cooked HTML content
+ * @returns {Promise} A promise that resolves when the post has been updated
+ */
+ async triggerChangedPost(postId, updatedAt, opts = {}) {
+ opts ||= {};
- const resolved = Promise.resolve();
if (!postId) {
- return resolved;
+ return;
}
const existing = this._identityMap[postId];
+
+ // Only fetch and update if the post exists and has a different updated timestamp
if (existing && existing.updated_at !== updatedAt) {
- const url = "/posts/" + postId;
- const store = this.store;
- return ajax(url).then((p) => {
- if (opts.preserveCooked) {
- p.cooked = existing.get("cooked");
- }
+ // Fetch the latest post data from the server
+ const updatedData = await ajax(`/posts/${postId}`);
- this.storePost(store.createRecord("post", p));
- });
+ // Preserve the existing cooked HTML content if requested
+ if (opts.preserveCooked) {
+ updatedData.cooked = existing.cooked;
+ }
+
+ // Create a new post record with updated data and store it in the identity map.
+ // Creating a new record will update the existing one in the map, which will then
+ // trigger re-rendering of UI components that use the tracked data that was updated.
+ const updatedPost = this.store.createRecord("post", updatedData);
+
+ // Update the post in the post stream's identity map
+ this.storePost(updatedPost);
}
- return resolved;
}
triggerLikedPost(postId, likesCount, userID, eventType) {
@@ -1098,7 +1113,9 @@ export default class PostStream extends RestModel {
return existing;
}
- post.set("topic", this.topic);
+ if (post.topic !== this.topic) {
+ post.topic = this.topic;
+ }
this._identityMap[post.get("id")] = post;
}
return post;
diff --git a/app/assets/javascripts/discourse/app/modifiers/d-autocomplete.js b/app/assets/javascripts/discourse/app/modifiers/d-autocomplete.js
new file mode 100644
index 0000000000000..52f3785d05939
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/modifiers/d-autocomplete.js
@@ -0,0 +1,622 @@
+import { tracked } from "@glimmer/tracking";
+import { registerDestructor } from "@ember/destroyable";
+import { action } from "@ember/object";
+import { cancel } from "@ember/runloop";
+import { service } from "@ember/service";
+import Modifier from "ember-modifier";
+import DAutocompleteResults from "discourse/components/d-autocomplete-results";
+import discourseDebounce from "discourse/lib/debounce";
+import { INPUT_DELAY } from "discourse/lib/environment";
+import { VISIBILITY_OPTIMIZERS } from "float-kit/lib/constants";
+
+export const SKIP = "skip";
+export const CANCELLED_STATUS = "__CANCELLED";
+
+/**
+ * Class-based modifier for adding autocomplete functionality to input elements
+ * Preserves exact CSS structure for backward compatibility
+ *
+ * @class DAutocompleteModifier
+ * @param {string} key - Trigger character (e.g., "@", "#", ":")
+ * @param {Function} dataSource - Async function to fetch results: (term) => Promise
+ * @param {Function} template - Template function that receives {options: results} and returns HTML
+ * @param {Function} [transformComplete] - Transform completion before insertion
+ * @param {Function} [afterComplete] - Callback after completion
+ * @param {boolean} [debounced=false] - Enable debounced search
+ * @param {boolean} [preserveKey=true] - Include trigger key in completion
+ * @param {boolean} [autoSelectFirstSuggestion=true] - Auto-select first result
+ * @param {Function} [triggerRule] - Function to determine if autocomplete should trigger: (element, opts) => Promise
+ * @param {Function} [onKeyUp] - Function to extract search patterns from text on keyup: (text, caretPosition) => Array
+ */
+export default class DAutocompleteModifier extends Modifier {
+ /**
+ * Static helper function to set up autocomplete on any element
+ *
+ * @param {Object} owner - Ember owner
+ * @param {HTMLElement} element - The element to modify with autocomplete functionality
+ * @param {Object} autocompleteHandler - Handler for text operations
+ * @param {Object} options - Autocomplete options
+ */
+ static setupAutocomplete(owner, element, autocompleteHandler, options) {
+ const modifier = new DAutocompleteModifier(owner, {
+ named: {},
+ positional: [],
+ });
+
+ const modifierOptions = {
+ ...options,
+ textHandler: autocompleteHandler,
+ };
+
+ modifier.modify(element, [modifierOptions]);
+ return modifier;
+ }
+
+ @service menu;
+
+ @tracked expanded = false;
+ @tracked results = [];
+ @tracked selectedIndex = -1;
+ @tracked searchTerm = "";
+ @tracked completeStart = null;
+ @tracked completeEnd = null;
+
+ // Internal state
+ previousTerm = null;
+ debouncedSearch = null;
+ targetElement = null;
+
+ // Constants
+ ALLOWED_LETTERS_REGEXP = /[\s[{(/+]/;
+ TRIGGER_CHAR_RELATIVE_OFFSET = 9;
+ VERTICAL_RELATIVE_OFFSET = 10;
+
+ constructor(owner, args) {
+ super(owner, args);
+ registerDestructor(this, (instance) => instance.cleanup());
+ }
+
+ @action
+ handleKeyUp(event) {
+ // Skip if modifier keys are pressed or other keys handled in KeyDown
+ if (
+ this.hasModifierKey(event) ||
+ ["Enter", "Escape", "Tab"].includes(event.key)
+ ) {
+ return;
+ }
+
+ if (this.shouldDebounce) {
+ this.debouncedSearch = discourseDebounce(
+ this,
+ this.performAutocomplete,
+ event,
+ INPUT_DELAY
+ );
+ return;
+ }
+ // Handle potential async errors without blocking the UI
+ this.performAutocomplete(event).catch((e) => {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] handleKeyup: ", e);
+ });
+ }
+
+ @action
+ async handleKeyDown(event) {
+ // Handle navigation when autocomplete is open
+ if (this.expanded) {
+ switch (event.key) {
+ case "ArrowUp":
+ event.preventDefault();
+ await this.moveSelection(-1);
+ break;
+ case "ArrowDown":
+ event.preventDefault();
+ await this.moveSelection(1);
+ break;
+ case "Enter":
+ case "Tab":
+ event.preventDefault();
+ if (this.selectedIndex >= 0) {
+ await this.selectResult(this.results[this.selectedIndex], event);
+ }
+ break;
+ case "Escape":
+ event.preventDefault();
+ event.stopPropagation();
+ await this.closeAutocomplete();
+ break;
+ case "ArrowRight":
+ // Allow right arrow to close autocomplete if at end of word
+ if (this.targetElement.value[this.getCaretPosition()] === " ") {
+ await this.closeAutocomplete();
+ }
+ break;
+ case "Backspace":
+ // Handle backspace to potentially reopen autocomplete
+ await this.handleBackspace(event);
+ break;
+ }
+ } else {
+ // Handle backspace when closed to potentially reopen,
+ // skip if modifier keys are pressed - this prevents autocomplete from opening on full deletion
+ if (event.key === "Backspace" && !this.hasModifierKey(event)) {
+ await this.handleBackspace(event);
+ }
+ }
+ }
+
+ @action
+ async handlePaste(event) {
+ // Trigger autocomplete check after paste with proper async handling
+ try {
+ // Use requestAnimationFrame for better performance than setTimeout - less flickering
+ await new Promise((resolve) => requestAnimationFrame(resolve));
+ await this.performAutocomplete(event);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] handlePaste: ", e);
+ }
+ }
+
+ @action
+ async handleGlobalClick() {
+ try {
+ if (this.expanded) {
+ await this.closeAutocomplete();
+ }
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] handleGlobalClick: ", e);
+ }
+ }
+
+ hasModifierKey(event) {
+ return event.ctrlKey || event.altKey || event.metaKey;
+ }
+
+ async shouldTrigger(opts = {}) {
+ if (!this.options.triggerRule) {
+ return true;
+ }
+
+ try {
+ const triggerContext = {
+ ...opts,
+ inCodeBlock: () => this.options.textHandler.inCodeBlock(),
+ };
+ const triggerRuleResult = await this.options.triggerRule(
+ this.targetElement,
+ triggerContext
+ );
+ return triggerRuleResult ?? true;
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] triggerRule error: ", e);
+ return true; // Default to allowing autocomplete on error
+ }
+ }
+
+ modify(element, [options]) {
+ this.targetElement = element;
+ this.options = options || {};
+
+ // Set up event listeners
+ element.addEventListener("keyup", this.handleKeyUp);
+ element.addEventListener("keydown", this.handleKeyDown);
+ element.addEventListener("paste", this.handlePaste);
+
+ // Global click handler to close autocomplete
+ document.addEventListener("click", this.handleGlobalClick);
+ }
+
+ @action
+ cleanup() {
+ cancel(this.debouncedSearch);
+ if (this.targetElement) {
+ this.targetElement.removeEventListener("keyup", this.handleKeyUp);
+ this.targetElement.removeEventListener("keydown", this.handleKeyDown);
+ this.targetElement.removeEventListener("paste", this.handlePaste);
+ }
+
+ document.removeEventListener("click", this.handleGlobalClick);
+ this.menu.close("d-autocomplete");
+ }
+
+ get shouldDebounce() {
+ return this.options.debounced ?? false;
+ }
+
+ // [introduced in https://github.com/discourse/discourse/commit/e02cc98092f5a889d0313cd741b29926be7430ab]
+ // By default, when the autocomplete popup is rendered it has the
+ // first suggestion 'selected', and pressing enter key inserts
+ // the first suggestion into the input box.
+ // If you want to stop that behavior, i.e. have the popup renders
+ // with no suggestions selected, set the `autoSelectFirstSuggestion`
+ // option to false.
+ // With this option set to false, users will have to select
+ // a suggestion via the up/down arrow keys and then press enter
+ // to insert it.
+ get autoSelectFirstSuggestion() {
+ return this.options.autoSelectFirstSuggestion ?? true;
+ }
+
+ async performAutocomplete() {
+ const caretPosition = this.getCaretPosition();
+ const value = this.getValue();
+ const key = value[caretPosition - 1];
+
+ // onKeyUp for additional custom trigger logic
+ if (this.options.key && this.options.onKeyUp && key !== this.options.key) {
+ const match = this.options.onKeyUp(value, caretPosition);
+ if (match && (await this.shouldTrigger())) {
+ this.completeStart = caretPosition - match[0].length;
+ this.completeEnd = caretPosition - 1;
+ const term = match[0].substring(1, match[0].length);
+ await this.performSearch(term);
+ return;
+ }
+ }
+
+ // Check if we should trigger autocomplete
+ if (this.completeStart === null && caretPosition > 0) {
+ // Try backwards scanning to find existing autocomplete context
+ const position = await this.guessCompletePosition();
+ if (position.completeStart !== null) {
+ this.completeStart = position.completeStart;
+ this.completeEnd = caretPosition - 1;
+ await this.performSearch(position.term || "");
+ } else if (key === this.options.key) {
+ // Fallback to original trigger logic for new autocomplete sessions
+ const prevChar = value.charAt(caretPosition - 2);
+ if (
+ (!prevChar || this.ALLOWED_LETTERS_REGEXP.test(prevChar)) &&
+ (await this.shouldTrigger())
+ ) {
+ this.completeStart = caretPosition - 1;
+ this.completeEnd = caretPosition - 1;
+ await this.performSearch("");
+ }
+ }
+ } else if (this.completeStart !== null) {
+ // Extract search term
+ const term = value.substring(
+ this.completeStart + (this.options.key ? 1 : 0),
+ caretPosition
+ );
+
+ // Validate we're still in autocomplete context
+ if (!this.options.key || value[this.completeStart] === this.options.key) {
+ this.completeEnd = caretPosition - 1;
+ await this.performSearch(term);
+ } else {
+ await this.closeAutocomplete();
+ }
+ }
+ }
+
+ async handleBackspace() {
+ try {
+ if (this.completeStart === null && this.options.key) {
+ const position = await this.guessCompletePosition({ backSpace: true });
+ if (position.completeStart !== null) {
+ this.completeStart = position.completeStart;
+ this.completeEnd = this.getCaretPosition() - 1;
+ await this.performAutocomplete();
+ }
+ }
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] handleBackspace: ", e);
+ }
+ }
+
+ async performSearch(term) {
+ if (this.isDestroying || this.isDestroyed) {
+ return;
+ }
+
+ // Skip if same term (basic caching)
+ if (this.previousTerm === term && !this.options.forceRefresh) {
+ return;
+ }
+
+ this.previousTerm = term;
+ this.searchTerm = term;
+
+ // Close if only whitespace or invalid context
+ if (
+ (term.length !== 0 && term.trim().length === 0) ||
+ this.getValue()[this.getCaretPosition()]?.trim()
+ ) {
+ await this.closeAutocomplete();
+ return;
+ }
+
+ const results = this.options.dataSource(term);
+
+ if (results && results.then && typeof results.then === "function") {
+ try {
+ const resolvedResults = await results;
+ this.updateResults(resolvedResults || []);
+ } catch (e) {
+ if (e.name !== "AbortError") {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] performSearch: ", e);
+ }
+ await this.closeAutocomplete();
+ }
+ return;
+ }
+
+ // For handling non-async dataSources
+ this.updateResults(results || []);
+ }
+
+ updateResults(results) {
+ if (
+ this.completeStart === null ||
+ results === SKIP ||
+ results === CANCELLED_STATUS
+ ) {
+ return;
+ }
+
+ const oldResults = this.results;
+ this.results = results;
+
+ if (!this.results || this.results.length === 0) {
+ this.closeAutocomplete();
+ return;
+ }
+
+ // If menu is already open, reactive getters update based on results
+ if (this.expanded) {
+ // This ensures we only reset the selected style if results changed between typing of search term
+ if (JSON.stringify(oldResults) !== JSON.stringify(results)) {
+ this.selectedIndex = this.autoSelectFirstSuggestion ? 0 : -1;
+ }
+ return;
+ }
+
+ this.openAutocomplete();
+ }
+
+ async openAutocomplete() {
+ this.selectedIndex = this.autoSelectFirstSuggestion ? 0 : -1;
+ try {
+ // Create virtual element positioned at the caret location
+ const virtualElement = this.createVirtualElementAtCaret();
+
+ const menuOptions = {
+ identifier: "d-autocomplete",
+ component: DAutocompleteResults,
+ visibilityOptimizer: VISIBILITY_OPTIMIZERS.AUTO_PLACEMENT,
+ placement: "top-start",
+ allowedPlacements: [
+ "top-start",
+ "top-end",
+ "bottom-start",
+ "bottom-end",
+ ],
+ data: {
+ getResults: () => this.results,
+ getSelectedIndex: () => this.selectedIndex,
+ onSelect: (result, index, event) => this.selectResult(result, event),
+ template: this.options.template,
+ onRender: this.options.onRender,
+ },
+ modalForMobile: false,
+ onClose: () => {
+ this.expanded = false;
+ this.options.onClose?.();
+ },
+ };
+
+ await this.menu.show(virtualElement, menuOptions);
+ this.expanded = true;
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] renderAutocomplete: ", e);
+ }
+ }
+
+ @action
+ async closeAutocomplete() {
+ await this.menu.close("d-autocomplete");
+
+ this.expanded = false;
+ this.completeStart = null;
+ this.completeEnd = null;
+ this.searchTerm = "";
+ this.results = [];
+ this.selectedIndex = -1;
+ this.previousTerm = null;
+
+ cancel(this.debouncedSearch);
+
+ // Note: onClose callback is handled by the menu's onClose option
+ }
+
+ @action
+ async moveSelection(direction) {
+ try {
+ if (this.results.length === 0) {
+ return;
+ }
+
+ // Calculate new selectedIndex
+ const newSelectedIndex = Math.max(
+ 0,
+ Math.min(this.results.length - 1, this.selectedIndex + direction)
+ );
+
+ // Only update if the index actually changed
+ if (newSelectedIndex !== this.selectedIndex) {
+ this.selectedIndex = newSelectedIndex;
+ }
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] moveSelection: ", e);
+ }
+ }
+
+ @action
+ async selectResult(result, event) {
+ try {
+ await this.completeTextareaTerm(result, event);
+ await this.closeAutocomplete();
+
+ // Clear any cached search state to prevent showing stale results
+ this.previousTerm = null;
+ this.searchTerm = "";
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] selectResult error: ", e);
+ }
+ }
+
+ @action
+ async completeTextareaTerm(term, event) {
+ if (!term) {
+ return;
+ }
+
+ // Transform if needed
+ if (this.options.transformComplete) {
+ term = await this.options.transformComplete(term, event);
+ }
+
+ if (!term) {
+ return;
+ }
+
+ const preserveKey = this.options.preserveKey ?? true;
+ const replacement = (preserveKey ? this.options.key || "" : "") + term;
+
+ // [introduced in https://github.com/discourse/discourse/commit/5fb6dd9bfaf6191393b7809fa0ac11b952a70a23]
+ // After completion is done our position for completeStart may have
+ // drifted. This can happen if the TEXTAREA changed out-of-band between
+ // the time autocomplete was first displayed and the time of completion
+ // Specifically this may happen due to uploads which inject a placeholder
+ // which is later replaced with a different length string.
+ const pos = await this.guessCompletePosition({ completeTerm: true });
+ let completeEnd;
+ let completeStart;
+
+ if (pos.completeStart !== undefined && pos.completeEnd !== undefined) {
+ completeStart = pos.completeStart;
+ completeEnd = pos.completeEnd;
+ } else {
+ completeStart = completeEnd = this.getCaretPosition();
+ }
+
+ // Use textHandler's replaceTerm method for consistent behavior
+ this.options.textHandler.replaceTerm(
+ completeStart,
+ completeEnd,
+ replacement
+ );
+
+ this.options.afterComplete?.(this.getValue(), event);
+ }
+
+ async guessCompletePosition(opts = {}) {
+ let prev, stopFound, term;
+ let prevIsGood = true;
+ let backSpace = opts?.backSpace;
+ let completeTermOption = opts?.completeTerm;
+ let caretPos = this.getCaretPosition();
+
+ if (backSpace) {
+ caretPos -= 1;
+ }
+
+ let start = null;
+ let end = null;
+ const initialCaretPos = caretPos;
+
+ while (prevIsGood && caretPos >= 0) {
+ caretPos -= 1;
+ prev = this.getValue()[caretPos];
+
+ stopFound = prev === this.options.key;
+
+ if (stopFound) {
+ prev = this.getValue()[caretPos - 1];
+ const shouldTrigger = await this.shouldTrigger({ backSpace });
+
+ if (
+ shouldTrigger &&
+ (prev === undefined || this.ALLOWED_LETTERS_REGEXP.test(prev))
+ ) {
+ start = caretPos;
+ term = this.getValue().substring(caretPos + 1, initialCaretPos);
+ end = caretPos + term.length;
+ break;
+ }
+ }
+
+ prevIsGood = !/\s/.test(prev);
+ if (completeTermOption) {
+ prevIsGood ||= prev === " ";
+ }
+ }
+
+ return { completeStart: start, completeEnd: end, term };
+ }
+
+ getValue() {
+ return this.options.textHandler.getValue();
+ }
+
+ getCaretPosition() {
+ return this.options.textHandler.getCaretPosition();
+ }
+
+ getAbsoluteCaretCoords() {
+ // Use textHandler for accurate relative coordinate calculation
+ if (this.options.textHandler && this.options.textHandler.getCaretCoords) {
+ try {
+ // Use completeStart position (where @ is) like legacy autocomplete does
+ const position =
+ this.completeStart !== null
+ ? this.completeStart
+ : this.getCaretPosition();
+ const relativeCoords =
+ this.options.textHandler.getCaretCoords(position);
+
+ // Convert to absolute viewport coordinates
+ const textareaRect = this.targetElement.getBoundingClientRect();
+
+ return {
+ x: textareaRect.left + relativeCoords.left,
+ y: textareaRect.top + relativeCoords.top,
+ };
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error("[autocomplete] getAbsoluteCaretCoords: ", e);
+ }
+ }
+
+ // Fallback: return textarea position (will be inaccurate but won't crash)
+ const textareaRect = this.targetElement.getBoundingClientRect();
+ return {
+ x: textareaRect.left,
+ y: textareaRect.top,
+ };
+ }
+
+ createVirtualElementAtCaret() {
+ const caretCoords = this.getAbsoluteCaretCoords();
+ return {
+ getBoundingClientRect: () => ({
+ left: caretCoords.x + this.TRIGGER_CHAR_RELATIVE_OFFSET,
+ top: caretCoords.y + this.VERTICAL_RELATIVE_OFFSET,
+ width: 1,
+ height: 10,
+ }),
+ };
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/services/element-classes.js b/app/assets/javascripts/discourse/app/services/element-classes.js
index 2d015bd0dd505..61bf50a73d380 100644
--- a/app/assets/javascripts/discourse/app/services/element-classes.js
+++ b/app/assets/javascripts/discourse/app/services/element-classes.js
@@ -4,20 +4,20 @@ import { disableImplicitInjections } from "discourse/lib/implicit-injections";
@disableImplicitInjections
export default class ElementClassesService extends Service {
- /** @type Map */
+ /** @type Map */
#helpers = new Map();
registerClasses(helper, element, classes) {
if (this.#helpers.has(helper)) {
- const previousClasses = this.#helpers.get(helper);
+ const previousClasses = this.#helpers.get(helper).classes;
- this.#helpers.set(helper, classes);
+ this.#helpers.set(helper, { classes, element });
this.removeUnusedClasses(element, previousClasses);
} else {
- this.#helpers.set(helper, classes);
+ this.#helpers.set(helper, { classes, element });
registerDestructor(helper, () => {
- const previousClasses = this.#helpers.get(helper);
+ const previousClasses = this.#helpers.get(helper).classes;
this.#helpers.delete(helper);
this.removeUnusedClasses(element, previousClasses);
});
@@ -29,7 +29,12 @@ export default class ElementClassesService extends Service {
}
removeUnusedClasses(element, classes) {
- const remainingClasses = new Set([...this.#helpers.values()].flat());
+ const remainingClasses = new Set(
+ ...this.#helpers
+ .values()
+ .filter(({ element: el }) => el === element)
+ .flatMap(({ classes: cls }) => cls)
+ );
for (const elementClass of classes) {
if (!remainingClasses.has(elementClass)) {
diff --git a/app/assets/javascripts/discourse/app/services/store.js b/app/assets/javascripts/discourse/app/services/store.js
index bc7544a84104f..3f1880a40cff6 100644
--- a/app/assets/javascripts/discourse/app/services/store.js
+++ b/app/assets/javascripts/discourse/app/services/store.js
@@ -1,3 +1,4 @@
+import { untrack } from "@glimmer/validator";
import { warn } from "@ember/debug";
import { set } from "@ember/object";
import Service from "@ember/service";
@@ -418,8 +419,26 @@ export default class StoreService extends Service {
klass = RestModel;
}
- existing.setProperties(klass.munge(obj));
+ const updatedProperties = klass.munge(obj);
+
+ // When running property comparisons, we need to prevent Glimmer from creating tracking contexts
+ // which would otherwise cause "already used in same computation" errors when the values update
+ untrack(() => {
+ // Only update properties whose values actually changed to optimize rerenders
+ // If a property value is unchanged, remove it from the update list
+ updatedProperties &&
+ Object.keys(updatedProperties).forEach((key) => {
+ if (existing[key] === updatedProperties[key]) {
+ delete updatedProperties[key];
+ }
+ });
+ });
+
+ // Apply all property updates in a single batch to trigger just one rerender
+ existing.setProperties(updatedProperties);
+
obj[adapter.primaryKey] = id;
+
return existing;
}
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/lib/text-manipulation.js b/app/assets/javascripts/discourse/app/static/prosemirror/lib/text-manipulation.js
index 46ae5b70582d9..ca898e2e383ab 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/lib/text-manipulation.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/lib/text-manipulation.js
@@ -1,6 +1,7 @@
// @ts-check
-import { setOwner } from "@ember/owner";
+import { getOwner, setOwner } from "@ember/owner";
import { next } from "@ember/runloop";
+import { service } from "@ember/service";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import $ from "jquery";
import { lift, setBlockType, toggleMark, wrapIn } from "prosemirror-commands";
@@ -9,6 +10,7 @@ import { liftListItem, sinkListItem } from "prosemirror-schema-list";
import { TextSelection } from "prosemirror-state";
import { bind } from "discourse/lib/decorators";
import escapeRegExp from "discourse/lib/escape-regexp";
+import DAutocompleteModifier from "discourse/modifiers/d-autocomplete";
import { i18n } from "discourse-i18n";
import { hasMark, inNode, isNodeActive } from "./plugin-utils";
@@ -21,6 +23,8 @@ import { hasMark, inNode, isNodeActive } from "./plugin-utils";
/** @implements {TextManipulation} */
export default class ProsemirrorTextManipulation {
+ @service siteSettings;
+
allowPreview = false;
/** @type {import("prosemirror-model").Schema} */
@@ -82,12 +86,21 @@ export default class ProsemirrorTextManipulation {
}
autocomplete(options) {
- // @ts-ignore
- $(this.view.dom).autocomplete(
- options instanceof Object
- ? { textHandler: this.autocompleteHandler, ...options }
- : options
- );
+ if (this.siteSettings.floatkit_autocomplete_composer) {
+ return DAutocompleteModifier.setupAutocomplete(
+ getOwner(this),
+ this.view.dom,
+ this.autocompleteHandler,
+ options
+ );
+ } else {
+ // @ts-ignore
+ $(this.view.dom).autocomplete(
+ options instanceof Object
+ ? { textHandler: this.autocompleteHandler, ...options }
+ : options
+ );
+ }
}
applySurroundSelection(head, tail, exampleKey) {
@@ -506,6 +519,7 @@ class ProsemirrorPlaceholderHandler {
}
progress() {}
+
progressComplete() {}
cancelAll() {
diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json
index 3bacd1f72cc0a..fea120560380b 100644
--- a/app/assets/javascripts/discourse/package.json
+++ b/app/assets/javascripts/discourse/package.json
@@ -81,7 +81,7 @@
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
"@popperjs/core": "^2.11.8",
- "@swc/core": "^1.13.2",
+ "@swc/core": "^1.13.3",
"@types/jquery": "^3.5.32",
"@types/qunit": "^2.19.12",
"@types/rsvp": "^4.0.9",
@@ -107,7 +107,7 @@
"ember-auto-import": "^2.10.0",
"ember-buffered-proxy": "^2.1.1",
"ember-cached-decorator-polyfill": "^1.0.2",
- "ember-cli": "~6.5.0",
+ "ember-cli": "~6.6.0",
"ember-cli-app-version": "^7.0.0",
"ember-cli-babel": "^8.2.0",
"ember-cli-deprecation-workflow": "^3.4.0",
diff --git a/app/assets/javascripts/discourse/tests/acceptance/category-banner-test.js b/app/assets/javascripts/discourse/tests/acceptance/category-banner-test.js
index a383e59a3a739..df8cfaf4c7cba 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/category-banner-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/category-banner-test.js
@@ -50,14 +50,11 @@ acceptance("Category Banners", function (needs) {
await visit("/c/test-read-only-with-banner");
await click("#create-topic");
- assert.dom(".d-editor").exists("opens composer");
-
- assert
- .dom(".d-editor .selected-name[data-name='test-read-only-with-banner']")
- .doesNotExist("does not show read-only category in composer");
+ assert.dom(".dialog-body").exists("pops up a modal");
+ await click(".dialog-footer .btn-primary");
+ assert.dom(".dialog-body").doesNotExist("closes the modal");
assert.dom(".category-read-only-banner").exists("shows a banner");
-
assert
.dom(".category-read-only-banner .inner")
.exists({ count: 1 }, "allows staff to embed html in the message");
diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js
index e0ad593a184a9..128ca971069a3 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js
@@ -21,7 +21,159 @@ acceptance("Composer - editor mentions", function (needs) {
};
needs.user();
- needs.settings({ enable_mentions: true, allow_uncategorized_topics: true });
+ needs.settings({
+ enable_mentions: true,
+ allow_uncategorized_topics: true,
+ });
+ needs.hooks.afterEach(() => clock?.restore());
+
+ needs.pretender((server, helper) => {
+ server.get("/t/11557.json", () => {
+ const topicFixture = cloneJSON(topicFixtures["/t/130.json"]);
+ topicFixture.id = 11557;
+ return helper.response(topicFixture);
+ });
+ server.get("/u/search/users", () => {
+ return helper.response({
+ users: [
+ {
+ username: "user",
+ name: "Some User",
+ avatar_template:
+ "https://avatars.discourse.org/v3/letter/t/41988e/{size}.png",
+ status,
+ },
+ {
+ username: "user2",
+ name: "Some User",
+ avatar_template:
+ "https://avatars.discourse.org/v3/letter/t/41988e/{size}.png",
+ },
+ {
+ username: "foo",
+ avatar_template:
+ "https://avatars.discourse.org/v3/letter/t/41988e/{size}.png",
+ },
+ ],
+ groups: [
+ {
+ name: "user_group",
+ full_name: "Group",
+ },
+ ],
+ });
+ });
+ });
+
+ test("selecting user mentions", async function (assert) {
+ this.siteSettings.floatkit_autocomplete_composer = false;
+ await visit("/");
+ await click("#create-topic");
+
+ await simulateKeys(".d-editor-input", "abc @u\r");
+
+ assert
+ .dom(".d-editor-input")
+ .hasValue("abc @user ", "replaces mention correctly");
+ });
+
+ test("selecting user mentions after deleting characters", async function (assert) {
+ this.siteSettings.floatkit_autocomplete_composer = false;
+ await visit("/");
+ await click("#create-topic");
+
+ await simulateKeys(".d-editor-input", "abc @user a\b\b\r");
+
+ assert
+ .dom(".d-editor-input")
+ .hasValue("abc @user ", "replaces mention correctly");
+ });
+
+ test("selecting user mentions after deleting characters mid sentence", async function (assert) {
+ this.siteSettings.floatkit_autocomplete_composer = false;
+ await visit("/");
+ await click("#create-topic");
+
+ await simulateKeys(".d-editor-input", "abc @user 123");
+ await setCaretPosition(".d-editor-input", 9);
+ await simulateKeys(".d-editor-input", "\b\b\r");
+
+ assert
+ .dom(".d-editor-input")
+ .hasValue("abc @user 123", "replaces mention correctly");
+ });
+
+ test("shows status on search results when mentioning a user", async function (assert) {
+ this.siteSettings.floatkit_autocomplete_composer = false;
+ const timezone = loggedInUser().user_option.timezone;
+ const now = moment(status.ends_at).add(-1, "hour").format();
+ clock = fakeTime(now, timezone, true);
+
+ await visit("/");
+ await click("#create-topic");
+
+ await simulateKeys(".d-editor-input", "@u");
+
+ assert
+ .dom(`.autocomplete .emoji[alt='${status.emoji}']`)
+ .exists("status emoji is shown");
+
+ assert
+ .dom(".autocomplete .user-status-message-description")
+ .hasText(status.description, "status description is shown");
+ });
+
+ test("metadata matches are moved to the end", async function (assert) {
+ this.siteSettings.floatkit_autocomplete_composer = false;
+ await visit("/");
+ await click("#create-topic");
+
+ await simulateKeys(".d-editor-input", "abc @u");
+
+ assert.deepEqual(
+ [...queryAll(".ac-user .username")].map((e) => e.innerText),
+ ["user", "user2", "user_group", "foo"]
+ );
+
+ await simulateKeys(".d-editor-input", "\bf");
+
+ assert.deepEqual(
+ [...queryAll(".ac-user .username")].map((e) => e.innerText),
+ ["foo", "user", "user2"]
+ );
+ });
+
+ test("shows users immediately when @ is typed in a reply", async function (assert) {
+ this.siteSettings.floatkit_autocomplete_composer = false;
+ await visit("/");
+ await click(".topic-list-item .title");
+ await click(".btn-primary.create");
+
+ await simulateKeys(".d-editor-input", "abc @");
+
+ assert.deepEqual(
+ [...document.querySelectorAll(".ac-user .username")].map(
+ (e) => e.innerText
+ ),
+ ["user_group", "user", "user2", "foo"]
+ );
+ });
+});
+
+acceptance("Composer - editor mentions with floatkit", function (needs) {
+ let clock = null;
+
+ const status = {
+ emoji: "tooth",
+ description: "off to dentist",
+ ends_at: "2100-02-01T09:00:00.000Z",
+ };
+
+ needs.user();
+ needs.settings({
+ enable_mentions: true,
+ allow_uncategorized_topics: true,
+ });
needs.hooks.afterEach(() => clock?.restore());
needs.pretender((server, helper) => {
diff --git a/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js b/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js
index 0120843ddb2fc..208d49eb80688 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js
@@ -20,6 +20,78 @@ acceptance("Emoji", function (needs) {
});
});
+ test("emoji is cooked properly", async function (assert) {
+ this.siteSettings.floatkit_autocomplete_composer = false;
+ await visit("/t/internationalization-localization/280");
+ await click("#topic-footer-buttons .btn.create");
+
+ await simulateKeys(".d-editor-input", "a :blonde_woman\t");
+ assert
+ .dom(".d-editor-preview")
+ .hasHtml(
+ `a 
`
+ );
+ });
+
+ test("emoji can be picked from the emoji-picker using the mouse", async function (assert) {
+ this.siteSettings.floatkit_autocomplete_composer = false;
+ await visit("/t/internationalization-localization/280");
+ await click("#topic-footer-buttons .btn.create");
+
+ await simulateKeys(".d-editor-input", "a :man_b");
+
+ // the 6th item in the list is the "more..."
+ await click(".autocomplete.ac-emoji ul li:nth-of-type(6) a");
+ await emojiPicker().select("man_biking");
+
+ assert
+ .dom(".d-editor-preview")
+ .hasHtml(
+ `a 
`
+ );
+ });
+
+ test("skin toned emoji is cooked properly", async function (assert) {
+ this.siteSettings.floatkit_autocomplete_composer = false;
+ await visit("/t/internationalization-localization/280");
+ await click("#topic-footer-buttons .btn.create");
+
+ await fillIn(".d-editor-input", "a :blonde_woman:t5:");
+
+ assert
+ .dom(".d-editor-preview")
+ .hasHtml(
+ `a 
`
+ );
+ });
+
+ needs.settings({ emoji_autocomplete_min_chars: 2 });
+
+ test("siteSetting:emoji_autocomplete_min_chars", async function (assert) {
+ this.siteSettings.floatkit_autocomplete_composer = false;
+ await visit("/t/internationalization-localization/280");
+ await click("#topic-footer-buttons .btn.create");
+
+ await simulateKeys(".d-editor-input", ":s");
+ assert.dom(".autocomplete.ac-emoji").doesNotExist();
+
+ await simulateKey(".d-editor-input", "w");
+ assert.dom(".autocomplete.ac-emoji").exists();
+ });
+});
+
+acceptance("Emoji with floatkit", function (needs) {
+ needs.user();
+
+ needs.pretender((server, helper) => {
+ server.get("/emojis/search-aliases.json", () => {
+ return helper.response([]);
+ });
+ server.get("/drafts/topic_280.json", function () {
+ return helper.response(200, { draft: null });
+ });
+ });
+
test("emoji is cooked properly", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#topic-footer-buttons .btn.create");
@@ -39,8 +111,8 @@ acceptance("Emoji", function (needs) {
await simulateKeys(".d-editor-input", "a :man_b");
- // the 5th item in the list is the "more..."
- await click(".autocomplete.ac-emoji ul li:nth-of-type(6)");
+ // the 6th item in the list is the "more..."
+ await click(".autocomplete.ac-emoji ul li:nth-of-type(6) a");
await emojiPicker().select("man_biking");
assert
diff --git a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js
index b2d31f26fc7af..b924c2f4f90f9 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js
@@ -228,14 +228,14 @@ acceptance("Tags listed by group", function (needs) {
);
});
- test("new topic button works when viewing staff-only tags", async function (assert) {
+ test("new topic button is not available for staff-only tags", async function (assert) {
updateCurrentUser({ moderator: false, admin: false });
await visit("/tag/regular-tag");
assert.dom("#create-topic").isEnabled();
await visit("/tag/staff-only-tag");
- assert.dom("#create-topic").isEnabled();
+ assert.dom("#create-topic").isDisabled();
updateCurrentUser({ moderator: true });
diff --git a/app/assets/javascripts/discourse/tests/integration/helpers/body-class-test.gjs b/app/assets/javascripts/discourse/tests/integration/helpers/element-class-test.gjs
similarity index 57%
rename from app/assets/javascripts/discourse/tests/integration/helpers/body-class-test.gjs
rename to app/assets/javascripts/discourse/tests/integration/helpers/element-class-test.gjs
index a889370de2fd1..301adb01ad2c6 100644
--- a/app/assets/javascripts/discourse/tests/integration/helpers/body-class-test.gjs
+++ b/app/assets/javascripts/discourse/tests/integration/helpers/element-class-test.gjs
@@ -1,9 +1,11 @@
+import { tracked } from "@glimmer/tracking";
import { render, settled } from "@ember/test-helpers";
import { module, test } from "qunit";
import bodyClass from "discourse/helpers/body-class";
+import htmlClass from "discourse/helpers/html-class";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
-module("Integration | Helper | body-class", function (hooks) {
+module("Integration | Helper | body-class and html-class", function (hooks) {
setupRenderingTest(hooks);
test("A single class", async function (assert) {
@@ -40,4 +42,33 @@ module("Integration | Helper | body-class", function (hooks) {
.dom(document.body)
.doesNotHaveClass("bar", "does not have .bar anymore");
});
+
+ test("HTML and body classes", async function (assert) {
+ const state = new (class {
+ @tracked condition = false;
+ })();
+
+ await render(
+
+ {{#if state.condition}}
+ {{bodyClass "my-class"}}
+ {{htmlClass "my-class"}}
+ {{/if}}
+
+ );
+ assert.dom(document.body).doesNotHaveClass("my-class");
+ assert.dom(document.documentElement).doesNotHaveClass("my-class");
+
+ state.condition = true;
+ await settled();
+
+ assert.dom(document.body).hasClass("my-class");
+ assert.dom(document.documentElement).hasClass("my-class");
+
+ state.condition = false;
+ await settled();
+
+ assert.dom(document.body).doesNotHaveClass("my-class");
+ assert.dom(document.documentElement).doesNotHaveClass("my-class");
+ });
});
diff --git a/app/assets/javascripts/discourse/tests/unit/lib/user-search-test.js b/app/assets/javascripts/discourse/tests/unit/lib/user-search-test.js
index d12ef6167a61b..309c6e31340f2 100644
--- a/app/assets/javascripts/discourse/tests/unit/lib/user-search-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/lib/user-search-test.js
@@ -1,7 +1,7 @@
import { setupTest } from "ember-qunit";
import { module, test } from "qunit";
-import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
import userSearch from "discourse/lib/user-search";
+import { CANCELLED_STATUS } from "discourse/modifiers/d-autocomplete";
import pretender, { response } from "discourse/tests/helpers/create-pretender";
module("Unit | Utility | user-search", function (hooks) {
diff --git a/app/assets/javascripts/float-kit/package.json b/app/assets/javascripts/float-kit/package.json
index 8af9442ebfa14..8b3cd91ff6bd9 100644
--- a/app/assets/javascripts/float-kit/package.json
+++ b/app/assets/javascripts/float-kit/package.json
@@ -32,7 +32,7 @@
"@types/qunit": "^2.19.12",
"@types/rsvp": "^4.0.9",
"broccoli-asset-rev": "^3.0.0",
- "ember-cli": "~6.5.0",
+ "ember-cli": "~6.6.0",
"ember-cli-inject-live-reload": "^2.1.0",
"ember-cli-sri": "^2.1.1",
"ember-cli-terser": "^4.0.2",
diff --git a/app/assets/javascripts/pretty-text/package.json b/app/assets/javascripts/pretty-text/package.json
index 8b9ab5ba7b3da..84159a35e00b9 100644
--- a/app/assets/javascripts/pretty-text/package.json
+++ b/app/assets/javascripts/pretty-text/package.json
@@ -29,7 +29,7 @@
"@types/qunit": "^2.19.12",
"@types/rsvp": "^4.0.9",
"broccoli-asset-rev": "^3.0.0",
- "ember-cli": "~6.5.0",
+ "ember-cli": "~6.6.0",
"ember-cli-inject-live-reload": "^2.1.0",
"ember-cli-sri": "^2.1.1",
"ember-cli-terser": "^4.0.2",
diff --git a/app/assets/javascripts/select-kit/package.json b/app/assets/javascripts/select-kit/package.json
index 36b055bbeeccf..4a6a831a7bff1 100644
--- a/app/assets/javascripts/select-kit/package.json
+++ b/app/assets/javascripts/select-kit/package.json
@@ -35,7 +35,7 @@
"@types/qunit": "^2.19.12",
"@types/rsvp": "^4.0.9",
"broccoli-asset-rev": "^3.0.0",
- "ember-cli": "~6.5.0",
+ "ember-cli": "~6.6.0",
"ember-cli-inject-live-reload": "^2.1.0",
"ember-cli-sri": "^2.1.1",
"ember-cli-terser": "^4.0.2",
diff --git a/app/assets/javascripts/theme-transpiler/package.json b/app/assets/javascripts/theme-transpiler/package.json
index 64bb33b71f600..7ae0d04e08df2 100644
--- a/app/assets/javascripts/theme-transpiler/package.json
+++ b/app/assets/javascripts/theme-transpiler/package.json
@@ -14,7 +14,7 @@
"@babel/preset-env": "^7.28.0",
"@babel/standalone": "^7.28.2",
"@csstools/postcss-light-dark-function": "^2.0.9",
- "@rollup/browser": "^4.46.1",
+ "@rollup/browser": "^4.46.2",
"@rollup/plugin-babel": "^6.0.4",
"abort-controller": "^3.0.0",
"autoprefixer": "^10.4.21",
diff --git a/app/assets/stylesheets/admin/admin_base.scss b/app/assets/stylesheets/admin/admin_base.scss
index 6158f9308dab5..ec1ed7ac07dc2 100644
--- a/app/assets/stylesheets/admin/admin_base.scss
+++ b/app/assets/stylesheets/admin/admin_base.scss
@@ -4,6 +4,10 @@
$mobile-breakpoint: 700px;
+:root {
+ --admin-content-max-width: min(700px, 100%);
+}
+
// Common admin styles
.admin-main-nav {
display: inline-flex;
diff --git a/app/assets/stylesheets/admin/admin_config_area.scss b/app/assets/stylesheets/admin/admin_config_area.scss
index 248ec7d479e4e..31fd014f81949 100644
--- a/app/assets/stylesheets/admin/admin_config_area.scss
+++ b/app/assets/stylesheets/admin/admin_config_area.scss
@@ -74,14 +74,11 @@
}
&__primary-content {
- flex: 0 1 70%;
+ flex: 0 1 100%;
display: flex;
flex-direction: column;
gap: var(--space-4);
-
- @media (max-width: $mobile-breakpoint) {
- flex: 0 1 auto;
- }
+ max-width: var(--admin-content-max-width);
}
&__aside {
diff --git a/app/assets/stylesheets/admin/customize.scss b/app/assets/stylesheets/admin/customize.scss
index 2d5c295dd9238..3e6a5f802fabe 100644
--- a/app/assets/stylesheets/admin/customize.scss
+++ b/app/assets/stylesheets/admin/customize.scss
@@ -144,6 +144,7 @@
.show-current-style {
display: inline-block;
vertical-align: top;
+ max-width: var(--admin-content-max-width);
.title {
font-family: var(--heading-font-family);
diff --git a/app/assets/stylesheets/admin/plugins.scss b/app/assets/stylesheets/admin/plugins.scss
index 2d3aa3c33688a..7c7ddefb324be 100644
--- a/app/assets/stylesheets/admin/plugins.scss
+++ b/app/assets/stylesheets/admin/plugins.scss
@@ -63,6 +63,10 @@
white-space: nowrap;
display: block;
}
+
+ &__preinstalled {
+ color: var(--primary-medium);
+ }
}
}
@@ -146,8 +150,5 @@
}
.admin-plugins-howto {
- a {
- display: inline-block;
- width: 100%;
- }
+ margin-left: 0.35em;
}
diff --git a/app/assets/stylesheets/color_definitions.scss b/app/assets/stylesheets/color_definitions.scss
index 0d071598d57e4..4e130249e9cdb 100644
--- a/app/assets/stylesheets/color_definitions.scss
+++ b/app/assets/stylesheets/color_definitions.scss
@@ -125,11 +125,14 @@
--gold: #{$gold};
--silver: #{$silver};
--bronze: #{$bronze};
+ --d-link-color: var(--tertiary);
--title-color--read: var(--primary-medium);
--content-border-color: var(--primary-low);
--input-border-color: var(--primary-400);
--table-border-color: var(--content-border-color);
--metadata-color: var(--primary-medium);
+ --d-badge-card-background-color: var(--primary-very-low);
+ --mention-background-color: var(--primary-low);
--title-color: var(--primary);
--title-color--header: var(--header_primary);
--excerpt-color: var(--primary-high);
diff --git a/app/assets/stylesheets/common/base/category-list.scss b/app/assets/stylesheets/common/base/category-list.scss
index 93c0d0737f9a9..494e5c3762c2d 100644
--- a/app/assets/stylesheets/common/base/category-list.scss
+++ b/app/assets/stylesheets/common/base/category-list.scss
@@ -127,6 +127,8 @@
border-left-width: 0;
border-style: solid;
border-color: var(--primary-low);
+ border-top-right-radius: var(--d-border-radius);
+ border-bottom-right-radius: var(--d-border-radius);
}
&.no-logos {
diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss
index 4f0fefad4ff53..072dcc4747aad 100644
--- a/app/assets/stylesheets/common/base/discourse.scss
+++ b/app/assets/stylesheets/common/base/discourse.scss
@@ -12,8 +12,6 @@
--d-input-bg-color--disabled: var(--primary-very-low);
--d-input-text-color--disabled: var(--primary-medium);
--d-input-border--disabled: 1px solid var(--primary-low);
- --d-input-padding-h: 0.65em;
- --d-input-padding-v: 0.5em;
--d-nav-color: var(--primary);
--d-nav-bg-color: transparent;
--d-nav-color--hover: var(--tertiary);
@@ -25,23 +23,23 @@
--d-nav-font-size: initial;
--d-input-focused-color: var(--tertiary);
--d-category-border-box-width: 2px;
- --d-category-box-background-color: none;
+ --d-category-box-background-color: var(--secondary);
--d-category-border-accent-width: 6px;
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--nav-space: var(--space-3);
--nav-horizontal-padding: 0em;
--d-main-content-gap: var(--space-8);
--main-outlet-padding-top: var(--space-6);
- --main-outlet-padding-v: 0em;
+ --main-outlet-padding-x: 0em;
--main-outlet-padding-bottom: 0em;
--table-border-width: 1px;
--d-topic-list-avatar-size: 24px;
--d-topic-list-title-font-size: var(--font-up-1);
--d-topic-list-metadata-top-space: var(--space-1);
- --d-topic-list-data-padding-h: var(--space-3);
- --d-topic-list-data-padding-v: var(--space-1);
- --d-topic-list-header-data-padding-h: var(--space-3);
- --d-topic-list-header-data-padding-v: var(--space-1);
+ --d-topic-list-data-padding-y: var(--space-3);
+ --d-topic-list-data-padding-x: var(--space-1);
+ --d-topic-list-header-data-padding-y: var(--space-3);
+ --d-topic-list-header-data-padding-x: var(--space-1);
--d-topic-list-likes-views-posts-width: 4.3em;
--d-topic-list-data-padding-inline-start: var(--space-3);
--d-topic-list-data-padding-inline-end: var(--space-3);
@@ -49,8 +47,8 @@
--category-boxes-text-alignment: center;
--d-topic-list-header-background-color: var(--secondary);
--d-topic-list-header-text-color: var(--primary-medium);
- --d-topic-list-margin-h: 0em;
- --d-topic-list-margin-v: 0em;
+ --d-topic-list-margin-y: 0em;
+ --d-topic-list-margin-x: 0em;
--d-topic-list-margin-bottom: 10px;
--d-tag-horizontal-padding: 0em;
--d-tag-font-weight: initial;
@@ -63,14 +61,14 @@
--category-boxes-title-font-size: var(--font-up-2);
--d-category-boxes-margin-top: var(--space-4);
--d-categories-list-title-margin-bottom: 0em;
- --d-header-padding-v: 0.67em;
+ --d-header-padding-x: 0.67em;
--d-table-border-top-height: 3px;
- --d-wrap-padding-h: 0.67em;
+ --d-wrap-padding-x: 0.67em;
--topic-title-font-weight: 400;
--topic-title-font-weight--visited: 400;
--topic-list-item-background-color: var(--secondary);
--topic-list-item-background-color--visited: var(--secondary);
- --list-container-horizontal-padding: 0em;
+ --list-container-padding-x: 0em;
--d-topic-list-header-font-size: initial;
}
@@ -472,7 +470,7 @@ textarea {
.discourse-no-touch & {
&:hover,
&:focus {
- background-color: var(--primary-low);
+ background-color: var(--d-hover);
}
}
@@ -517,7 +515,7 @@ textarea {
max-width: var(--d-max-width);
margin-right: auto;
margin-left: auto;
- padding: 0 var(--d-wrap-padding-h);
+ padding: 0 var(--d-wrap-padding-x);
@include viewport.until(sm) {
min-width: 0;
@@ -799,7 +797,7 @@ form {
}
#main-outlet {
- padding: var(--main-outlet-padding-top) var(--main-outlet-padding-v)
+ padding: var(--main-outlet-padding-top) var(--main-outlet-padding-x)
var(--main-outlet-padding-bottom);
.mobile-view & {
diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss
index be1577443c062..acb446e612cbc 100644
--- a/app/assets/stylesheets/common/base/header.scss
+++ b/app/assets/stylesheets/common/base/header.scss
@@ -24,7 +24,7 @@
box-sizing: border-box;
width: 100%;
height: 100%;
- padding: 0 var(--d-header-padding-v);
+ padding: 0 var(--d-header-padding-x);
.contents {
display: flex;
@@ -171,7 +171,6 @@
padding: 0.2143em;
text-decoration: none;
cursor: pointer;
- border: 1px solid transparent;
outline: none;
img.avatar {
@@ -181,20 +180,12 @@
.discourse-no-touch &:hover,
.discourse-no-touch &:focus {
- background-color: var(--primary-low);
- border-top: 1px solid transparent;
- border-left: 1px solid transparent;
- border-right: 1px solid transparent;
+ background-color: var(--d-hover);
> .d-icon {
color: var(--primary-medium);
}
}
-
- &:active {
- color: var(--primary);
- background-color: var(--primary-low);
- }
}
.drop-down-mode & {
diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss
index 74fb49425e71b..b8fd3bb114d64 100644
--- a/app/assets/stylesheets/common/base/menu-panel.scss
+++ b/app/assets/stylesheets/common/base/menu-panel.scss
@@ -278,10 +278,8 @@
.menu-tabs-container {
display: flex;
flex-direction: column;
- padding: var(--space-half);
overflow-y: auto;
overscroll-behavior: contain;
- margin-inline: calc(var(--user-menu-border-width) * -1);
}
.tabs-list {
@@ -292,20 +290,22 @@
display: flex;
position: relative;
border-radius: 0;
- padding: 0.8125em;
+ padding: 0.375em; // 6px
@include viewport.until(sm) {
- padding: 1.2em 1.4em;
+ padding: 0.75em 0.875em; // 12px 14px
}
@media screen and (height <= 400px) {
// helps with 400% zoom level
font-size: var(--font-down-1);
- padding: 0.5em 0.875em;
+ padding: 0.125em 0.375em; // 2px 6px
}
.d-icon {
+ border-radius: var(--d-border-radius);
color: var(--primary-medium);
+ padding: 0.5em; // 8px
}
.badge-notification {
@@ -317,29 +317,24 @@
@media screen and (height <= 400px) {
// helps with 400% zoom level
- right: 0;
+ right: 3px;
top: 0;
}
}
&.active {
.d-icon {
- box-shadow: var(--active-shadow);
color: var(--d-sidebar-active-color);
background-color: var(--d-sidebar-active-background);
- border-radius: calc(var(--d-border-radius) * 0.25);
}
}
&:not(.active):hover,
&:not(.active):focus-visible {
background: none;
- padding-left: calc(0.875em - var(--user-menu-border-width));
.d-icon {
- box-shadow: var(--hover-shadow);
background-color: var(--d-sidebar-highlight-background);
- border-radius: calc(var(--d-border-radius) * 0.25);
}
}
}
@@ -679,6 +674,7 @@
> div {
display: flex;
flex-direction: column;
+ overflow: hidden;
}
}
diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss
index b49c75bd0adb0..2d724b1d01574 100644
--- a/app/assets/stylesheets/common/base/onebox.scss
+++ b/app/assets/stylesheets/common/base/onebox.scss
@@ -1,11 +1,16 @@
@use "lib/viewport";
+:root {
+ --onebox-shadow-color: var(--primary-100);
+ --onebox-border-color: var(--primary-300);
+}
+
@mixin onebox-shadow($thickness) {
border: 0;
margin-inline: $thickness;
box-shadow:
- 0 0 0 1px var(--primary-300),
- 0 0 0 $thickness var(--primary-100);
+ 0 0 0 1px var(--onebox-border-color),
+ 0 0 0 $thickness var(--onebox-shadow-color);
// Makes the outer drop shadow radius equal to --d-border-radius
border-radius: calc(var(--d-border-radius) - ($thickness / 2));
diff --git a/app/assets/stylesheets/common/base/sidebar-section-link.scss b/app/assets/stylesheets/common/base/sidebar-section-link.scss
index 88f7e43fc4d71..cc37d9739406b 100644
--- a/app/assets/stylesheets/common/base/sidebar-section-link.scss
+++ b/app/assets/stylesheets/common/base/sidebar-section-link.scss
@@ -45,7 +45,7 @@
&.active {
background: var(--d-sidebar-active-background);
color: var(--d-sidebar-active-color);
- font-weight: bold;
+ font-weight: var(--d-sidebar-active-font-weight);
.sidebar-section-link-prefix {
&.icon {
diff --git a/app/assets/stylesheets/common/base/sidebar.scss b/app/assets/stylesheets/common/base/sidebar.scss
index 647fd49ffee55..5c0d0282bdb9d 100644
--- a/app/assets/stylesheets/common/base/sidebar.scss
+++ b/app/assets/stylesheets/common/base/sidebar.scss
@@ -50,6 +50,7 @@
--d-sidebar-active-icon-color: var(--d-sidebar-link-color);
--d-sidebar-active-prefix-background: var(--primary-200);
--d-sidebar-active-suffix-color: var(--tertiary-med-or-tertiary);
+ --d-sidebar-active-font-weight: bold; // example: active section link
@include viewport.until(sm) {
--d-sidebar-row-height: 2.4em;
diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss
index ace37c3db9a60..07a73255a97cf 100644
--- a/app/assets/stylesheets/common/base/user-badges.scss
+++ b/app/assets/stylesheets/common/base/user-badges.scss
@@ -132,7 +132,7 @@
}
.badge-card {
- background-color: var(--primary-very-low);
+ background-color: var(--d-badge-card-background-color);
border: 1px solid var(--content-border-color);
position: relative;
border-radius: var(--d-border-radius);
diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss
index 302c809701196..0ecf0f213afb0 100644
--- a/app/assets/stylesheets/common/base/user.scss
+++ b/app/assets/stylesheets/common/base/user.scss
@@ -17,7 +17,7 @@
grid-row-end: 2;
.horizontal-overflow-nav {
- border-block: 1px solid var(--primary-low);
+ border-block: 1px solid var(--content-border-color);
}
.group-dropdown {
@@ -36,7 +36,7 @@
grid-column-end: 3;
grid-row-start: 2;
grid-row-end: 3;
- border-bottom: 1px solid var(--primary-low);
+ border-bottom: 1px solid var(--content-border-color);
font-size: var(--font-down-1);
}
@@ -120,7 +120,7 @@
.user-notifications-filter {
display: block;
width: 100%;
- border-bottom: 0.5px solid var(--primary-low);
+ border-bottom: 0.5px solid var(--content-border-color);
}
}
@@ -146,7 +146,7 @@
.secondary {
display: inline-block;
width: 100%;
- border-top: 1px solid var(--primary-low);
+ border-top: 1px solid var(--content-border-color);
.btn {
padding: 4px 12px;
@@ -198,7 +198,7 @@
.details {
background: rgb(var(--secondary-rgb), 0.8);
- border-bottom: 1px solid var(--primary-low);
+ border-bottom: 1px solid var(--content-border-color);
.groups {
display: inline;
@@ -836,12 +836,12 @@
.pref-passkeys,
.pref-auth-tokens {
.row {
- border-top: 1px solid var(--primary-low);
+ border-top: 1px solid var(--content-border-color);
padding: 0.5em 0;
margin: 0.5em 0;
&:last-child {
- border-bottom: 1px solid var(--primary-low);
+ border-bottom: 1px solid var(--content-border-color);
}
}
}
@@ -895,7 +895,7 @@
width: 100%;
display: flex;
justify-content: space-between;
- border-top: 1px solid var(--primary-low);
+ border-top: 1px solid var(--content-border-color);
margin: 0.25em 0;
padding: 0.25em 0;
align-items: center;
@@ -928,7 +928,7 @@
.wrapper {
display: flex;
- border: 1px solid var(--primary-low);
+ border: 1px solid var(--content-border-color);
width: 100%;
}
diff --git a/app/assets/stylesheets/common/components/_index.scss b/app/assets/stylesheets/common/components/_index.scss
index 3f06c6a5a695e..72dfd9860ffe4 100644
--- a/app/assets/stylesheets/common/components/_index.scss
+++ b/app/assets/stylesheets/common/components/_index.scss
@@ -17,6 +17,7 @@
@import "calendar-date-time-input";
@import "composer-toggle-switch";
@import "convert-to-public-topic-modal";
+@import "d-autocomplete";
@import "d-toggle-switch";
@import "date-input";
@import "date-picker";
diff --git a/app/assets/stylesheets/common/components/autocomplete.scss b/app/assets/stylesheets/common/components/autocomplete.scss
index 8d445a8f16638..d586f614dfb5d 100644
--- a/app/assets/stylesheets/common/components/autocomplete.scss
+++ b/app/assets/stylesheets/common/components/autocomplete.scss
@@ -2,7 +2,6 @@
.autocomplete {
z-index: z("composer", "dropdown") + 1;
- position: absolute;
max-width: 370px;
min-width: 300px;
background-color: var(--secondary);
diff --git a/app/assets/stylesheets/common/components/d-autocomplete.scss b/app/assets/stylesheets/common/components/d-autocomplete.scss
new file mode 100644
index 0000000000000..fde79e4eb6b5e
--- /dev/null
+++ b/app/assets/stylesheets/common/components/d-autocomplete.scss
@@ -0,0 +1,30 @@
+@use "lib/viewport";
+
+.fk-d-menu[data-identifier="d-autocomplete"] {
+ z-index: z("modal", "dialog") + 1;
+ animation: fade-in ease 0.25s 1 forwards !important;
+
+ --d-border-radius: var(--space-2);
+
+ // Override compose.scss autocomplete styles to remove double styling
+ // Let d-menu handle the container border/shadow/radius
+ .autocomplete {
+ border: none;
+ box-shadow: none;
+ border-radius: 0;
+
+ ul {
+ li {
+ &:first-of-type a {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ &:last-of-type a {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/common/components/hashtag.scss b/app/assets/stylesheets/common/components/hashtag.scss
index 166f3eba0c724..b8c3a648d3c87 100644
--- a/app/assets/stylesheets/common/components/hashtag.scss
+++ b/app/assets/stylesheets/common/components/hashtag.scss
@@ -1,19 +1,3 @@
-a.hashtag {
- color: var(--primary-or-primary-low-mid);
- font-weight: bold;
-
- &:visited,
- &:hover {
- color: var(--primary-or-primary-low-mid);
- }
-
- &:hover {
- span {
- text-decoration: underline;
- }
- }
-}
-
.hashtag-cooked {
@include mention;
@@ -27,36 +11,23 @@ a.hashtag {
height: 0.72em;
display: inline-block;
color: var(--primary-medium);
+ margin-right: var(--space-1);
}
- .d-icon,
- .hashtag-icon-placeholder {
- font-size: var(--font-down-1);
- margin: 0;
- }
-
- img.emoji {
+ img.emoji,
+ svg {
width: 0.93em;
height: 0.93em;
- vertical-align: middle;
- }
-
- svg {
- display: inline;
+ vertical-align: -0.125em;
+ margin-right: var(--space-1);
}
.hashtag-category-square {
- flex: 0 0 auto;
+ display: inline-block;
width: 0.72em;
height: 0.72em;
- margin-right: 0.25em;
margin-left: 0.1em;
- display: inline-block;
- }
-
- .hashtag-category-icon,
- .hashtag-category-emoji {
- display: inline-block;
+ margin-right: var(--space-1);
}
}
@@ -95,28 +66,28 @@ a.hashtag {
}
&__link {
- align-items: center;
- display: flex;
-
- .d-icon {
- margin-right: 0.25em;
+ .autocomplete & {
+ // silly but needed to overrule the autocomplete styles
+ gap: var(--space-2);
}
- .hashtag-category-square,
.hashtag-category-icon,
.hashtag-category-emoji {
- flex: 0 0 auto;
+ width: 1em;
+ height: 1em;
+ }
+
+ .hashtag-category-square {
width: 0.93em;
height: 0.93em;
- margin-right: 5px;
display: inline-block;
}
- .hashtag-category-icon .svg-icon,
- .hashtag-category-emoji .emoji {
+ img.emoji,
+ svg {
width: 0.93em;
height: 0.93em;
- vertical-align: middle;
+ vertical-align: top;
}
}
diff --git a/app/assets/stylesheets/common/components/user-stream-item.scss b/app/assets/stylesheets/common/components/user-stream-item.scss
index 050653003a0e6..62e983f65dc79 100644
--- a/app/assets/stylesheets/common/components/user-stream-item.scss
+++ b/app/assets/stylesheets/common/components/user-stream-item.scss
@@ -8,7 +8,7 @@
.item,
.user-stream-item {
background: var(--d-content-background, var(--secondary));
- border-bottom: 1px solid var(--primary-low);
+ border-bottom: 1px solid var(--content-border-color);
padding: 1em 0.53em;
list-style: none;
@@ -121,7 +121,7 @@
li.notification {
padding: var(--space-3);
- border-bottom: 1px solid var(--primary-low);
+ border-bottom: 1px solid var(--content-border-color);
a {
align-items: center;
diff --git a/app/assets/stylesheets/common/foundation/base.scss b/app/assets/stylesheets/common/foundation/base.scss
index 0f557f0cdbd00..54999375c920b 100644
--- a/app/assets/stylesheets/common/foundation/base.scss
+++ b/app/assets/stylesheets/common/foundation/base.scss
@@ -59,22 +59,13 @@ html {
// Links
// --------------------------------------------------
-a {
- color: var(--tertiary);
+a,
+a:visited,
+a:hover,
+a:active {
+ color: var(--d-link-color);
text-decoration: none;
cursor: pointer;
-
- &:visited {
- color: var(--tertiary);
- }
-
- &:hover {
- color: var(--tertiary);
- }
-
- &:active {
- color: var(--tertiary);
- }
}
// Typography
diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss
index 59cc79a0dfc65..073005d3420f4 100644
--- a/app/assets/stylesheets/common/foundation/mixins.scss
+++ b/app/assets/stylesheets/common/foundation/mixins.scss
@@ -246,14 +246,13 @@ $hpad: 0.65em;
}
@mixin mention() {
- display: inline-flex;
- align-items: center;
- gap: 0.25em;
+ // memo to self: do not make this inline-flex because it breaks text alignments and causes a whole avalance of unpleasant consequences
+ display: inline-block;
font-size: 0.93em;
font-weight: normal;
color: var(--primary);
padding: 0.2em 0.34em;
- background: var(--primary-low);
+ background: var(--mention-background-color);
border-radius: 0.6em;
text-decoration: none;
text-wrap: nowrap;
diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss
index 8b0d08d9861c3..1f8a5e1c78fd7 100644
--- a/app/assets/stylesheets/desktop/topic-list.scss
+++ b/app/assets/stylesheets/desktop/topic-list.scss
@@ -26,7 +26,7 @@
.topic-list {
@extend .topic-list-icons;
- margin: var(--d-topic-list-margin-h) var(--d-topic-list-margin-v)
+ margin: var(--d-topic-list-margin-y) var(--d-topic-list-margin-x)
var(--d-topic-list-margin-bottom);
.topic-list-header {
@@ -34,14 +34,14 @@
}
.topic-list-header .topic-list-data {
- padding: var(--d-topic-list-header-data-padding-h)
- var(--d-topic-list-header-data-padding-v);
+ padding: var(--d-topic-list-header-data-padding-y)
+ var(--d-topic-list-header-data-padding-x);
color: var(--d-topic-list-header-text-color);
}
.topic-list-data {
- padding: var(--d-topic-list-data-padding-h)
- var(--d-topic-list-data-padding-v);
+ padding: var(--d-topic-list-data-padding-y)
+ var(--d-topic-list-data-padding-x);
&:first-of-type {
padding-inline-start: var(--d-topic-list-data-padding-inline-start);
@@ -317,5 +317,13 @@
.container.list-container {
position: relative;
- padding: 0 var(--list-container-horizontal-padding);
+ padding: 0 var(--list-container-padding-x);
+}
+
+.container.list-container.--categories {
+ padding: 0 var(--list-container-categories-padding-x);
+}
+
+.container.list-container.--topic-list {
+ padding: 0 var(--list-container-topiclist-padding-x);
}
diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss
index 48771e21ed6ff..e56b29bef9627 100644
--- a/app/assets/stylesheets/mobile/topic-post.scss
+++ b/app/assets/stylesheets/mobile/topic-post.scss
@@ -408,8 +408,8 @@ span.highlighted {
.read-state {
// contained within the padding to prevent vertical overflow
- max-width: var(--d-wrap-padding-h);
- right: calc(var(--d-wrap-padding-h) * -1);
+ max-width: var(--d-wrap-padding-x);
+ right: calc(var(--d-wrap-padding-x) * -1);
font-size: 6px; // static size to avoid overflow issues
svg {
diff --git a/app/models/post.rb b/app/models/post.rb
index 7b2f036ac89b4..7157cae2a2224 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -706,6 +706,8 @@ def unhide!
should_update_user_stat = false
end
+ self.topic.reset_bumped_at(self) if is_last_reply? && !whisper?
+
# We need to do this because TopicStatusUpdater also does the increment
# and we don't want to double count for the OP.
UserStatCountUpdater.increment!(self) if should_update_user_stat
diff --git a/app/models/topic.rb b/app/models/topic.rb
index 348f136bd6b5d..b541ed8f7782e 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -1922,11 +1922,13 @@ def is_category_topic?
@is_category_topic ||= Category.exists?(topic_id: self.id.to_i)
end
- def reset_bumped_at(post_id = nil)
+ def reset_bumped_at(post_or_post_id = nil)
post =
- (
- if post_id
- Post.find_by(id: post_id)
+ if post_or_post_id.is_a?(Post)
+ post_or_post_id
+ else
+ if post_or_post_id
+ Post.find_by(id: post_or_post_id)
else
ordered_posts.where(
user_deleted: false,
@@ -1934,7 +1936,7 @@ def reset_bumped_at(post_id = nil)
post_type: Post.types[:regular],
).last || first_post
end
- )
+ end
return if !post
diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb
index 4bdefe81e1c66..7e2253e27f59c 100644
--- a/app/serializers/user_option_serializer.rb
+++ b/app/serializers/user_option_serializer.rb
@@ -43,7 +43,8 @@ class UserOptionSerializer < ApplicationSerializer
:sidebar_link_to_filtered_list,
:sidebar_show_count_of_new_items,
:watched_precedence_over_muted,
- :topics_unread_when_closed
+ :topics_unread_when_closed,
+ :composition_mode
def auto_track_topics_after_msecs
object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs
diff --git a/app/views/user_notifications/digest.html.erb b/app/views/user_notifications/digest.html.erb
index edb2434333c6c..db3bf05ae5419 100644
--- a/app/views/user_notifications/digest.html.erb
+++ b/app/views/user_notifications/digest.html.erb
@@ -39,10 +39,8 @@
<%= render partial: "user_notifications/digest/popular_posts" %>
- <%= digest_custom_html("above_popular_topics") %>
<%= render partial: "user_notifications/digest/new_topics" %>
- <%= digest_custom_html("below_popular_topics") %>
<%= render partial: "user_notifications/digest/styles" %>
diff --git a/app/views/user_notifications/digest/_footer.text.erb b/app/views/user_notifications/digest/_footer.text.erb
index 2ed5b2cd364ab..92db8e32127d4 100644
--- a/app/views/user_notifications/digest/_footer.text.erb
+++ b/app/views/user_notifications/digest/_footer.text.erb
@@ -1,7 +1,5 @@
<%- site_link = raw(@markdown_linker.create(@site_name, '/')) %>
-<%= digest_custom_text("below_popular_topics") %>
-
<%= raw(@markdown_linker.references) %>
<%= digest_custom_text("above_footer") %>
diff --git a/app/views/user_notifications/digest/_popular_posts.text.erb b/app/views/user_notifications/digest/_popular_posts.text.erb
index 3340c37754ac6..7e98d91bf7112 100644
--- a/app/views/user_notifications/digest/_popular_posts.text.erb
+++ b/app/views/user_notifications/digest/_popular_posts.text.erb
@@ -1,10 +1,9 @@
<%- if @popular_posts.present? %>
-### <%=t 'user_notifications.digest.popular_posts' %>
+ ### <%=t 'user_notifications.digest.popular_posts' %>
-<%- @popular_posts.each_with_index do |post,i| %>
-<%= post.user.username -%> - <%= raw(@markdown_linker.create(post.topic.title, post.topic.url)) %>
+ <%- @popular_posts.each_with_index do |post,i| %>
+ <%= post.user.username -%> - <%= raw(@markdown_linker.create(post.topic.title, post.topic.url)) %>
- <%= raw(post.excerpt(1000, strip_links: true, text_entities: true, markdown_images: true)) %>
-
-<%- end %>
+ <%= raw(post.excerpt(1000, strip_links: true, text_entities: true, markdown_images: true)) %>
+ <%- end %>
<%- end %>
\ No newline at end of file
diff --git a/app/views/user_notifications/digest/_popular_topics.html.erb b/app/views/user_notifications/digest/_popular_topics.html.erb
index 8fa3608cc3b63..0bae53568f2c6 100644
--- a/app/views/user_notifications/digest/_popular_topics.html.erb
+++ b/app/views/user_notifications/digest/_popular_topics.html.erb
@@ -2,6 +2,8 @@
|
+ <%= digest_custom_html("above_popular_topics") %>
+
<% @popular_topics.each_with_index do |t, i| %>
<%= render partial: "user_notifications/digest/popular_topic", locals: { topic: t } %>
@@ -9,6 +11,8 @@
<%= digest_custom_html("below_post_#{i+1}") %>
<% end %>
<% end %>
+
+ <%= digest_custom_html("below_popular_topics") %>
|
diff --git a/app/views/user_notifications/digest/_popular_topics.text.erb b/app/views/user_notifications/digest/_popular_topics.text.erb
index 47888041504b4..73a44de82b913 100644
--- a/app/views/user_notifications/digest/_popular_topics.text.erb
+++ b/app/views/user_notifications/digest/_popular_topics.text.erb
@@ -1,14 +1,16 @@
<%- if @popular_topics.present? %>
-### <%=t 'user_notifications.digest.popular_topics' %>
+ <%= digest_custom_text("above_popular_topics") %>
+ ### <%=t 'user_notifications.digest.popular_topics' %>
-<%- @popular_topics.each_with_index do |t,i| %>
-<%= raw(@markdown_linker.create(t.title, t.url)) %>
+ <%- @popular_topics.each_with_index do |t,i| %>
+ <%= raw(@markdown_linker.create(t.title, t.url)) %>
-<%- if t.best_post.present? %>
- <%= raw(t.best_post.excerpt(1000, strip_links: true, text_entities: true, markdown_images: true)) %>
+ <%- if t.best_post.present? %>
+ <%= raw(t.best_post.excerpt(1000, strip_links: true, text_entities: true, markdown_images: true)) %>
+ <%- end %>
-<%- end %>
-<%= digest_custom_text("below_post_#{i+1}") %>
-<%- end %>
-<%- end %>
-<%= digest_custom_text("above_popular_topics") %>
\ No newline at end of file
+ <%= digest_custom_text("below_post_#{i+1}") %>
+ <%- end %>
+
+ <%= digest_custom_text("below_popular_topics") %>
+<%- end %>
\ No newline at end of file
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index cbae1ab35778d..9c63e444b4122 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -7090,7 +7090,7 @@ en:
dark_mode_warning: "You're currently using dark mode. When setting an active color palette the colors will be applied to the light mode of the theme. If you want to change the default dark mode colors, please edit the dark mode site setting."
title: "Colors"
edit: "Edit"
- set_default: "Set as active on %{theme}"
+ set_default: "Activate on default theme (%{theme})"
set_default_success: "%{schemeName} set as active palette for %{themeName}"
saved_refreshing: "Saved! Refreshing colors..."
from_theme: "From theme: %{name}"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 76bd123dbf3bc..6b2234840c0b7 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -2725,6 +2725,7 @@ en:
glimmer_post_stream_mode: "Control whether the new 'glimmer' post stream implementation is used. 'auto' will enable automatically once all your themes and plugins are ready. This implementation is under active development, and is not intended for production use. Do not develop themes/plugins against it until the implementation is finalized and announced."
glimmer_post_stream_mode_auto_groups: "Enable the new 'glimmer' post stream implementation in 'auto' mode for the specified user groups. This implementation is under active development, and is not intended for production use. Do not develop themes/plugins against it until the implementation is finalized and announced."
deactivate_widgets_rendering: "Disable the legacy widgets rendering engine. This will disable all widgets that have not been updated to Glimmer components, it will also force the Glimmer Post Stream to be enabled unconditionally regardless of the value of the `glimmer_post_stream_mode` setting. This setting is not recommended for production sites, as it may break existing themes and plugins."
+ floatkit_autocomplete_composer: "Enable the Floatkit-based autocomplete menu in composer. This is an UI enhancement that will replace the current JQuery-based autocomplete menu."
experimental_form_templates: "Enable the form templates feature. Manage the templates at Customize / Templates."
show_preview_for_form_templates: "Enable the preview for form templates feature"
lazy_load_categories_groups: "Lazy load category information only for users of these groups. This improves performance on sites with many categories."
@@ -5866,3 +5867,4 @@ en:
duplicate_ids: "has duplicate ids"
reserved_id: "has a reserved keyword as id: %{id}"
unsafe_description: "has an unsafe HTML description"
+ invalid_tag_group: "has invalid tag group: %{tag_group_name}"
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 28347f64de270..c92f6b89cf84b 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -569,6 +569,10 @@ basic:
enum: "InterfaceColorSelectorSetting"
default: "disabled"
area: "interface"
+ floatkit_autocomplete_composer:
+ client: true
+ default: true
+ area: "interface"
login:
invite_only:
diff --git a/lib/validators/form_template_yaml_validator.rb b/lib/validators/form_template_yaml_validator.rb
index ae230451eaa7a..508b2206aa1c0 100644
--- a/lib/validators/form_template_yaml_validator.rb
+++ b/lib/validators/form_template_yaml_validator.rb
@@ -31,6 +31,8 @@ def validate(record)
check_ids(record, field, existing_ids)
check_descriptions_html(record, field)
end
+
+ check_tag_groups(record, yaml.map { |f| f["tag_group"] })
rescue Psych::SyntaxError
record.errors.add(:template, I18n.t("form_templates.errors.invalid_yaml"))
end
@@ -81,4 +83,17 @@ def check_ids(record, field, existing_ids)
existing_ids << field["id"] if field["id"].present?
end
+
+ def check_tag_groups(record, tag_group_names)
+ tag_group_names = tag_group_names.compact.map(&:downcase).uniq
+ valid_tag_group_names =
+ TagGroup.where("lower(name) IN (?)", tag_group_names).pluck(:name).map(&:downcase)
+ invalid_tag_groups = tag_group_names - valid_tag_group_names
+ invalid_tag_groups.each do |tag_group_name|
+ record.errors.add(
+ :template,
+ I18n.t("form_templates.errors.invalid_tag_group", tag_group_name: tag_group_name),
+ )
+ end
+ end
end
diff --git a/package.json b/package.json
index be63794c22887..bc2f61a3015f3 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
"@glint/environment-ember-template-imports": "1.4.1-unstable.34c4510",
"@glint/template": "1.4.1-unstable.34c4510",
"@rdil/parallel-prettier": "^3.0.0",
- "@swc/core": "^1.13.2",
+ "@swc/core": "^1.13.3",
"chart.js": "3.5.1",
"chartjs-plugin-datalabels": "2.2.0",
"chrome-launcher": "^1.2.0",
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-composer.gjs
index 2f5b6bb41aca1..21442bdb54717 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-composer.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.gjs
@@ -25,7 +25,6 @@ import UpsertHyperlink from "discourse/components/modal/upsert-hyperlink";
import PluginOutlet from "discourse/components/plugin-outlet";
import concatClass from "discourse/helpers/concat-class";
import lazyHash from "discourse/helpers/lazy-hash";
-import { SKIP } from "discourse/lib/autocomplete";
import renderEmojiAutocomplete from "discourse/lib/autocomplete/emoji";
import userAutocomplete from "discourse/lib/autocomplete/user";
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
@@ -41,6 +40,7 @@ import {
} from "discourse/lib/user-status-on-autocomplete";
import virtualElementFromTextRange from "discourse/lib/virtual-element-from-text-range";
import { waitForClosedKeyboard } from "discourse/lib/wait-for-keyboard";
+import { SKIP } from "discourse/modifiers/d-autocomplete";
import { i18n } from "discourse-i18n";
import Button from "discourse/plugins/chat/discourse/components/chat/composer/button";
import ChatComposerDropdown from "discourse/plugins/chat/discourse/components/chat-composer-dropdown";
diff --git a/plugins/chat/assets/stylesheets/common/chat-composer.scss b/plugins/chat/assets/stylesheets/common/chat-composer.scss
index 40fa71a2eeeb4..f508c6c471da3 100644
--- a/plugins/chat/assets/stylesheets/common/chat-composer.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-composer.scss
@@ -1,3 +1,9 @@
+:root {
+ --d-chat-input-border-color: var(--primary-low);
+ --d-chat-input-bg-color: rgb(var(--primary-very-low-rgb), 0.5);
+ --d-chat-input-focused-shadow: 0 0 1px 0 var(--tertiary);
+}
+
.chat-composer {
&__wrapper {
display: flex;
@@ -46,14 +52,14 @@
box-sizing: border-box;
width: 100%;
flex-direction: row;
- border: 1px solid var(--content-border-color);
+ border: 1px solid var(--d-chat-input-border-color);
border-radius: var(--d-border-radius-large);
- background: rgb(var(--primary-very-low-rgb), 0.5);
+ background: var(--d-chat-input-bg-color);
min-height: 50px;
overflow: hidden;
.chat-composer.is-focused & {
- box-shadow: 0 0 1px 0 var(--tertiary);
+ box-shadow: var(--d-chat-input-focused-shadow);
}
.chat-composer.is-disabled & {
diff --git a/plugins/chat/assets/stylesheets/common/chat-message-separator.scss b/plugins/chat/assets/stylesheets/common/chat-message-separator.scss
index fd094bd03d67f..3b26670913b37 100644
--- a/plugins/chat/assets/stylesheets/common/chat-message-separator.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-message-separator.scss
@@ -129,7 +129,7 @@
box-sizing: border-box;
.chat-message-separator__line {
- border-top: 1px solid var(--secondary-high);
+ border-top: 1px solid var(--content-border-color);
margin: 0 0 -1px;
}
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-navbar.scss b/plugins/chat/assets/stylesheets/common/chat-navbar.scss
index 66d121a9e8e71..58b5091d16d15 100644
--- a/plugins/chat/assets/stylesheets/common/chat-navbar.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-navbar.scss
@@ -1,6 +1,6 @@
.c-navbar-container {
position: relative;
- border-bottom: 1px solid var(--primary-low);
+ border-bottom: 1px solid var(--content-border-color);
background: var(--secondary);
box-sizing: border-box;
display: flex;
diff --git a/plugins/chat/assets/stylesheets/desktop/base-desktop.scss b/plugins/chat/assets/stylesheets/desktop/base-desktop.scss
index 2b7569aa31a4c..bbc31dd8a7293 100644
--- a/plugins/chat/assets/stylesheets/desktop/base-desktop.scss
+++ b/plugins/chat/assets/stylesheets/desktop/base-desktop.scss
@@ -73,8 +73,8 @@
}
.full-page-chat {
- border-left: 1px solid var(--primary-low);
- border-right: 1px solid var(--primary-low);
+ border-left: 1px solid var(--content-border-color);
+ border-right: 1px solid var(--content-border-color);
}
&.has-sidebar-page .full-page-chat {
diff --git a/plugins/chat/spec/system/shortcuts/full_page_spec.rb b/plugins/chat/spec/system/shortcuts/full_page_spec.rb
index 93b7d9eab3522..4de162e79bb1d 100644
--- a/plugins/chat/spec/system/shortcuts/full_page_spec.rb
+++ b/plugins/chat/spec/system/shortcuts/full_page_spec.rb
@@ -5,6 +5,7 @@
fab!(:current_user, :user)
let(:chat) { PageObjects::Pages::Chat.new }
+ let(:channel_page) { PageObjects::Pages::ChatChannel.new }
before do
chat_system_bootstrap
@@ -19,7 +20,7 @@
page.send_keys("e")
- expect(find(".chat-composer__input").value).to eq("e")
+ expect(channel_page.composer).to have_value("e")
end
end
end
diff --git a/plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_usage_controller.rb b/plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_usage_controller.rb
index ea57cd4a37dfc..c4ac9dfeb641b 100644
--- a/plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_usage_controller.rb
+++ b/plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_usage_controller.rb
@@ -15,16 +15,33 @@ def report
private
def create_report
+ user_timezone = params[:timezone] || Time.zone.name
+ start_date = parse_date_in_timezone(params[:start_date], user_timezone) || 30.days.ago
+ end_date = parse_date_in_timezone(params[:end_date], user_timezone) || Time.current
+
report =
DiscourseAi::Completions::Report.new(
- start_date: params[:start_date]&.to_date || 30.days.ago,
- end_date: params[:end_date]&.to_date || Time.current,
+ start_date: start_date,
+ end_date: end_date,
+ timezone: user_timezone,
)
report = report.filter_by_feature(params[:feature]) if params[:feature].present?
report = report.filter_by_model(params[:model]) if params[:model].present?
report
end
+
+ def parse_date_in_timezone(date_string, timezone)
+ return nil unless date_string
+
+ # Parse date string in user's timezone
+ Time.zone = timezone
+ Time.zone.parse(date_string)
+ rescue StandardError
+ nil
+ ensure
+ Time.zone = nil # Reset to default
+ end
end
end
end
diff --git a/plugins/discourse-ai/app/jobs/regular/localize_categories.rb b/plugins/discourse-ai/app/jobs/regular/localize_categories.rb
index 45476a589fef9..ab143ea278f5b 100644
--- a/plugins/discourse-ai/app/jobs/regular/localize_categories.rb
+++ b/plugins/discourse-ai/app/jobs/regular/localize_categories.rb
@@ -11,17 +11,17 @@ def execute(args)
limit = args[:limit]
raise Discourse::InvalidParameters.new(:limit) if limit.nil?
return if limit <= 0
- locales = SiteSetting.content_localization_supported_locales.split("|")
- categories = Category.where("locale IS NOT NULL")
- if SiteSetting.ai_translation_backfill_limit_to_public_content
- categories = categories.where(read_restricted: false)
- end
- categories = categories.order(:id).limit(limit)
+ categories =
+ DiscourseAi::Translation::CategoryCandidates
+ .get
+ .where("locale IS NOT NULL")
+ .order(:id)
+ .limit(limit)
return if categories.empty?
remaining_limit = limit
-
+ locales = SiteSetting.content_localization_supported_locales.split("|")
categories.each do |category|
break if remaining_limit <= 0
diff --git a/plugins/discourse-ai/app/jobs/regular/localize_posts.rb b/plugins/discourse-ai/app/jobs/regular/localize_posts.rb
index 062974fdbb0f0..0c4ef3f9309d4 100644
--- a/plugins/discourse-ai/app/jobs/regular/localize_posts.rb
+++ b/plugins/discourse-ai/app/jobs/regular/localize_posts.rb
@@ -14,41 +14,18 @@ def execute(args)
locales = SiteSetting.content_localization_supported_locales.split("|")
locales.each do |locale|
base_locale = locale.split("_").first
+
posts =
- Post
+ DiscourseAi::Translation::PostCandidates
+ .get
.joins(
"LEFT JOIN post_localizations pl ON pl.post_id = posts.id AND pl.locale LIKE '#{base_locale}%'",
)
- .where(
- "posts.created_at > ?",
- SiteSetting.ai_translation_backfill_max_age_days.days.ago,
- )
- .where(deleted_at: nil)
- .where("posts.user_id > 0")
- .where.not(raw: [nil, ""])
.where.not(locale: nil)
.where("posts.locale NOT LIKE '#{base_locale}%'")
.where("pl.id IS NULL")
-
- posts = posts.joins(:topic)
-
- if SiteSetting.ai_translation_backfill_limit_to_public_content
- # exclude all PMs
- # and only include posts from public categories
- posts =
- posts
- .where.not(topics: { archetype: Archetype.private_message })
- .where(topics: { category_id: Category.where(read_restricted: false).select(:id) })
- else
- # all regular topics, and group PMs
- posts =
- posts.where(
- "topics.archetype != ? OR topics.id IN (SELECT topic_id FROM topic_allowed_groups)",
- Archetype.private_message,
- )
- end
-
- posts = posts.order(updated_at: :desc).limit(limit)
+ .order(updated_at: :desc)
+ .limit(limit)
next if posts.empty?
diff --git a/plugins/discourse-ai/app/jobs/regular/localize_topics.rb b/plugins/discourse-ai/app/jobs/regular/localize_topics.rb
index 0fa334d3c2e33..cad7ed15f160f 100644
--- a/plugins/discourse-ai/app/jobs/regular/localize_topics.rb
+++ b/plugins/discourse-ai/app/jobs/regular/localize_topics.rb
@@ -15,37 +15,16 @@ def execute(args)
locales.each do |locale|
base_locale = locale.split("_").first
topics =
- Topic
+ DiscourseAi::Translation::TopicCandidates
+ .get
.joins(
"LEFT JOIN topic_localizations tl ON tl.topic_id = topics.id AND tl.locale LIKE '#{base_locale}%'",
)
- .where(
- "topics.created_at > ?",
- SiteSetting.ai_translation_backfill_max_age_days.days.ago,
- )
- .where(deleted_at: nil)
- .where("topics.user_id > 0")
.where.not(locale: nil)
.where("topics.locale NOT LIKE '#{base_locale}%'")
.where("tl.id IS NULL")
-
- if SiteSetting.ai_translation_backfill_limit_to_public_content
- # exclude all PMs
- # and only include posts from public categories
- topics =
- topics
- .where.not(archetype: Archetype.private_message)
- .where(category_id: Category.where(read_restricted: false).select(:id))
- else
- # all regular topics, and group PMs
- topics =
- topics.where(
- "topics.archetype != ? OR topics.id IN (SELECT topic_id FROM topic_allowed_groups)",
- Archetype.private_message,
- )
- end
-
- topics = topics.order(updated_at: :desc).limit(limit)
+ .order(updated_at: :desc)
+ .limit(limit)
next if topics.empty?
diff --git a/plugins/discourse-ai/assets/javascripts/discourse/components/ai-usage.gjs b/plugins/discourse-ai/assets/javascripts/discourse/components/ai-usage.gjs
index 0aaac6b2fc264..0b5869ace52f5 100644
--- a/plugins/discourse-ai/assets/javascripts/discourse/components/ai-usage.gjs
+++ b/plugins/discourse-ai/assets/javascripts/discourse/components/ai-usage.gjs
@@ -25,6 +25,7 @@ import ComboBox from "select-kit/components/combo-box";
export default class AiUsage extends Component {
@service store;
+ @service currentUser;
@tracked startDate = moment().subtract(30, "days").toDate();
@tracked endDate = new Date();
@@ -48,6 +49,8 @@ export default class AiUsage extends Component {
data: {
start_date: moment(this.startDate).format("YYYY-MM-DD"),
end_date: moment(this.endDate).format("YYYY-MM-DD"),
+ timezone:
+ this.currentUser?.user_option?.timezone || moment.tz.guess(),
feature: this.selectedFeature,
model: this.selectedModel,
},
diff --git a/plugins/discourse-ai/db/migrate/20240611170905_move_embeddings_to_single_table_per_type.rb b/plugins/discourse-ai/db/migrate/20240611170905_move_embeddings_to_single_table_per_type.rb
index fad7070b730cc..576f86d81eede 100644
--- a/plugins/discourse-ai/db/migrate/20240611170905_move_embeddings_to_single_table_per_type.rb
+++ b/plugins/discourse-ai/db/migrate/20240611170905_move_embeddings_to_single_table_per_type.rb
@@ -145,12 +145,6 @@ def up
SELECT rag_document_fragment_id, 8, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
FROM ai_document_fragment_embeddings_8_1;
SQL
-
- begin
- DiscourseAi::Embeddings::VectorRepresentations::Base.current_representation
- rescue StandardError => e
- Rails.logger.error("Failed to index embeddings: #{e}")
- end
end
def down
diff --git a/plugins/discourse-ai/lib/completions/report.rb b/plugins/discourse-ai/lib/completions/report.rb
index e90ca12e9892f..46f0c57383705 100644
--- a/plugins/discourse-ai/lib/completions/report.rb
+++ b/plugins/discourse-ai/lib/completions/report.rb
@@ -5,11 +5,16 @@ class Report
UNKNOWN_FEATURE = "unknown"
USER_LIMIT = 50
- attr_reader :start_date, :end_date, :base_query
+ attr_reader :start_date, :end_date, :base_query, :timezone
- def initialize(start_date: 30.days.ago, end_date: Time.current)
+ def initialize(start_date: 30.days.ago, end_date: Time.current, timezone: Time.zone.name)
+ @timezone = timezone
+
+ Time.zone = timezone # Set the timezone for parsing dates in the user's timezone
@start_date = start_date.beginning_of_day
@end_date = end_date.end_of_day
+ Time.zone = nil # Reset to default timezone
+
@base_query = AiApiAuditLog.where(created_at: @start_date..@end_date)
end
@@ -100,16 +105,23 @@ def guess_period(period = nil)
def tokens_by_period(period = nil)
period = guess_period(period)
- base_query
- .group("DATE_TRUNC('#{period}', created_at)")
- .order("DATE_TRUNC('#{period}', created_at)")
- .select(
- "DATE_TRUNC('#{period}', created_at) as period",
- "SUM(COALESCE(request_tokens + response_tokens, 0)) as total_tokens",
- "SUM(COALESCE(cached_tokens,0)) as total_cached_tokens",
- "SUM(COALESCE(request_tokens,0)) as total_request_tokens",
- "SUM(COALESCE(response_tokens,0)) as total_response_tokens",
- )
+ results =
+ base_query
+ .group("DATE_TRUNC('#{period}', created_at)")
+ .order("DATE_TRUNC('#{period}', created_at)")
+ .select(
+ "DATE_TRUNC('#{period}', created_at) as period",
+ "SUM(COALESCE(request_tokens + response_tokens, 0)) as total_tokens",
+ "SUM(COALESCE(cached_tokens,0)) as total_cached_tokens",
+ "SUM(COALESCE(request_tokens,0)) as total_request_tokens",
+ "SUM(COALESCE(response_tokens,0)) as total_response_tokens",
+ )
+
+ # Convert periods to user's timezone
+ results.map do |row|
+ row.period = row.period.in_time_zone(timezone)
+ row
+ end
end
def user_breakdown
diff --git a/plugins/discourse-ai/lib/translation/base_candidates.rb b/plugins/discourse-ai/lib/translation/base_candidates.rb
new file mode 100644
index 0000000000000..bd976256f6b1c
--- /dev/null
+++ b/plugins/discourse-ai/lib/translation/base_candidates.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module DiscourseAi
+ module Translation
+ class BaseCandidates
+ COMPLETION_CACHE_TTL = 1.hour
+
+ # ModelType that are eligible for translation based on site settings
+ # @return [ActiveRecord::Relation] the ActiveRecord relation of the candidates
+ def self.get
+ raise NotImplementedError
+ end
+
+ # The completion in float percentage for the given locale
+ # @param locale [String] the locale for which to calculate the completion percentage
+ # @return [Float] the completion percentage for the given locale e.g. 0.75 for 75%
+ def self.get_completion_per_locale(locale)
+ Discourse
+ .cache
+ .fetch(get_completion_cache_key(locale), expires_in: COMPLETION_CACHE_TTL) do
+ done, total = calculate_completion_per_locale(locale)
+ return 1.0 if total.zero?
+ done / total.to_f
+ end
+ end
+
+ def self.clear_completion_cache(locale)
+ Discourse.cache.delete(get_completion_cache_key(locale))
+ end
+
+ private
+
+ # This method should return [completed_translations, total_needed_translations]
+ #
+ # This allows flexibility for the implementation to determine how the completion percentage is calculated.
+ # # @param locale [String] the locale for which to calculate the completion percentage
+ # @return [integer, integer]
+ def self.calculate_completion_per_locale(locale)
+ raise NotImplementedError
+ end
+
+ def self.completion_cache_key_for_type
+ raise NotImplementedError
+ end
+
+ def self.get_completion_cache_key(locale)
+ "#{completion_cache_key_for_type}_#{locale}"
+ end
+ end
+ end
+end
diff --git a/plugins/discourse-ai/lib/translation/category_candidates.rb b/plugins/discourse-ai/lib/translation/category_candidates.rb
new file mode 100644
index 0000000000000..b0196f0692ff8
--- /dev/null
+++ b/plugins/discourse-ai/lib/translation/category_candidates.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module DiscourseAi
+ module Translation
+ class CategoryCandidates < BaseCandidates
+ def self.get
+ categories = Category.all
+ if SiteSetting.ai_translation_backfill_limit_to_public_content
+ categories = categories.where(read_restricted: false)
+ end
+ categories
+ end
+
+ private
+
+ def self.calculate_completion_per_locale(locale)
+ base_locale = "#{locale.split("_").first}%"
+ sql = <<~SQL
+ WITH eligible_categories AS (
+ #{get.to_sql}
+ ),
+ total_count AS (
+ SELECT COUNT(*) AS count FROM eligible_categories
+ ),
+ done_count AS (
+ SELECT COUNT(DISTINCT c.id)
+ FROM eligible_categories c
+ LEFT JOIN category_localizations cl ON c.id = cl.category_id AND cl.locale LIKE :base_locale
+ WHERE c.locale LIKE :base_locale OR cl.category_id IS NOT NULL
+ )
+ SELECT d.count AS done, t.count AS total
+ FROM total_count t, done_count d
+ SQL
+
+ DB.query_single(sql, base_locale: "#{base_locale}%")
+ end
+
+ def self.completion_cache_key_for_type
+ "discourse_ai::translation::category_candidates"
+ end
+ end
+ end
+end
diff --git a/plugins/discourse-ai/lib/translation/post_candidates.rb b/plugins/discourse-ai/lib/translation/post_candidates.rb
new file mode 100644
index 0000000000000..855dd6125e5c4
--- /dev/null
+++ b/plugins/discourse-ai/lib/translation/post_candidates.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module DiscourseAi
+ module Translation
+ class PostCandidates < BaseCandidates
+ # Posts that are eligible for translation based on site settings
+ def self.get
+ posts =
+ Post
+ .where(
+ "posts.created_at > ?",
+ SiteSetting.ai_translation_backfill_max_age_days.days.ago,
+ )
+ .where(deleted_at: nil)
+ .where("posts.user_id > 0")
+ .where.not(raw: [nil, ""])
+
+ posts = posts.joins(:topic)
+ if SiteSetting.ai_translation_backfill_limit_to_public_content
+ # exclude all PMs
+ # and only include posts from public categories
+ posts =
+ posts
+ .where.not(topics: { archetype: Archetype.private_message })
+ .where(topics: { category_id: Category.where(read_restricted: false).select(:id) })
+ else
+ # all regular topics, and group PMs
+ posts =
+ posts.where(
+ "topics.archetype != ? OR topics.id IN (SELECT topic_id FROM topic_allowed_groups)",
+ Archetype.private_message,
+ )
+ end
+ end
+
+ private
+
+ def self.calculate_completion_per_locale(locale)
+ base_locale = "#{locale.split("_").first}%"
+
+ sql = <<~SQL
+ WITH eligible_posts AS (
+ #{get.to_sql}
+ ),
+ total_count AS (
+ SELECT COUNT(*) AS count FROM eligible_posts
+ ),
+ done_count AS (
+ SELECT COUNT(DISTINCT p.id)
+ FROM eligible_posts p
+ LEFT JOIN post_localizations pl ON p.id = pl.post_id AND pl.locale LIKE :base_locale
+ WHERE p.locale LIKE :base_locale OR pl.post_id IS NOT NULL
+ )
+ SELECT d.count AS done, t.count AS total
+ FROM total_count t, done_count d
+ SQL
+
+ DB.query_single(sql, base_locale: "#{base_locale}%")
+ end
+
+ def self.completion_cache_key_for_type
+ "discourse_ai::translation::post_candidates"
+ end
+ end
+ end
+end
diff --git a/plugins/discourse-ai/lib/translation/topic_candidates.rb b/plugins/discourse-ai/lib/translation/topic_candidates.rb
new file mode 100644
index 0000000000000..52c8ac2eb1e3c
--- /dev/null
+++ b/plugins/discourse-ai/lib/translation/topic_candidates.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module DiscourseAi
+ module Translation
+ class TopicCandidates < BaseCandidates
+ def self.get
+ topics =
+ Topic
+ .where(
+ "topics.created_at > ?",
+ SiteSetting.ai_translation_backfill_max_age_days.days.ago,
+ )
+ .where(deleted_at: nil)
+ .where("topics.user_id > 0")
+
+ if SiteSetting.ai_translation_backfill_limit_to_public_content
+ # exclude all PMs
+ # and only include topics from public categories
+ topics =
+ topics
+ .where.not(archetype: Archetype.private_message)
+ .where(category_id: Category.where(read_restricted: false).select(:id))
+ else
+ # all regular topics, and group PMs
+ topics =
+ topics.where(
+ "topics.archetype != ? OR topics.id IN (SELECT topic_id FROM topic_allowed_groups)",
+ Archetype.private_message,
+ )
+ end
+ end
+
+ private
+
+ def self.calculate_completion_per_locale(locale)
+ base_locale = locale.split("_").first
+
+ sql = <<~SQL
+ WITH eligible_topics AS (
+ #{get.to_sql}
+ ),
+ total_count AS (
+ SELECT COUNT(*) AS count FROM eligible_topics
+ ),
+ done_count AS (
+ SELECT COUNT(DISTINCT t.id)
+ FROM eligible_topics t
+ LEFT JOIN topic_localizations tl ON t.id = tl.topic_id AND tl.locale LIKE :base_locale
+ WHERE t.locale LIKE :base_locale OR tl.topic_id IS NOT NULL
+ )
+ SELECT d.count AS done, t.count AS total
+ FROM total_count t, done_count d
+ SQL
+
+ DB.query_single(sql, base_locale: "#{base_locale}%")
+ end
+
+ def self.completion_cache_key_for_type
+ "discourse_ai::translation::topic_candidates"
+ end
+ end
+ end
+end
diff --git a/plugins/discourse-ai/spec/lib/translation/category_candidates_spec.rb b/plugins/discourse-ai/spec/lib/translation/category_candidates_spec.rb
new file mode 100644
index 0000000000000..7e3de0bb5fb4b
--- /dev/null
+++ b/plugins/discourse-ai/spec/lib/translation/category_candidates_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+describe DiscourseAi::Translation::CategoryCandidates do
+ describe ".get" do
+ it "returns all categories" do
+ expect(DiscourseAi::Translation::CategoryCandidates.get.count).to eq(Category.count)
+ end
+
+ it "filters out read restricted categories if ai_translation_backfill_limit_to_public_content is enabled" do
+ SiteSetting.ai_translation_backfill_limit_to_public_content = true
+ restricted_category = Fabricate(:category, read_restricted: true)
+ public_category = Fabricate(:category, read_restricted: false)
+
+ categories = DiscourseAi::Translation::CategoryCandidates.get
+ expect(categories).not_to include(restricted_category)
+ expect(categories).to include(public_category)
+ end
+ end
+
+ describe ".get_completion_per_locale" do
+ context "when (scenario A) percentage determined by category's locale" do
+ it "returns 100% completion if all categories are in the locale" do
+ locale = "pt_BR"
+ Fabricate(:category, locale:)
+ Category.update_all(locale: locale)
+ Fabricate(:category, locale: "pt")
+
+ completion = DiscourseAi::Translation::CategoryCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1.0)
+ end
+
+ it "returns X% completion if some categories are in the locale" do
+ locale = "pt_BR"
+ Fabricate(:category, locale:)
+ Fabricate(:category, locale: "not_pt")
+
+ completion = DiscourseAi::Translation::CategoryCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1 / Category.count.to_f)
+ end
+ end
+
+ context "when (scenario B) percentage determined by category localizations" do
+ it "returns 100% completion if all categories have a localization in the locale" do
+ locale = "pt_BR"
+ Fabricate(:category)
+ Category.all.each { |category| Fabricate(:category_localization, category:, locale:) }
+ Fabricate(:category_localization, locale: "pt")
+
+ completion = DiscourseAi::Translation::CategoryCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1.0)
+ end
+
+ it "returns X% completion if some categories have a localization in the locale" do
+ locale = "es"
+ Fabricate(:category_localization, locale:)
+ Fabricate(:category_localization, locale: "pt")
+
+ completion = DiscourseAi::Translation::CategoryCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1 / Category.count.to_f)
+ end
+ end
+
+ it "returns the correct percentage based on (scenario A & B) `category.locale` and `CategoryLocalization` in the specified locale" do
+ locale = "pt_BR"
+
+ # translated candidates
+ Fabricate(:category, locale:)
+ category2 = Fabricate(:category)
+ Fabricate(:category_localization, category: category2, locale:)
+
+ # untranslated candidate
+ category3 = Fabricate(:category)
+ Fabricate(:category_localization, category: category3, locale: "zh_CN")
+
+ # not a candidate as it is read restricted
+ SiteSetting.ai_translation_backfill_limit_to_public_content = true
+ category4 = Fabricate(:category, read_restricted: true)
+ Fabricate(:category_localization, category: category4, locale:)
+
+ completion = DiscourseAi::Translation::CategoryCandidates.get_completion_per_locale(locale)
+ translated_candidates = 2 # category1 + category2
+ total_candidates = Category.count - 1 # excluding the read restricted category
+ expect(completion).to eq(translated_candidates / total_candidates.to_f)
+ end
+
+ it "does not exceed 100% completion when category.locale and category_localization both exist" do
+ locale = "pt_BR"
+ Category.update_all(locale:)
+ category = Fabricate(:category, locale:)
+ Fabricate(:category_localization, category:, locale:)
+
+ completion = DiscourseAi::Translation::CategoryCandidates.get_completion_per_locale(locale)
+ expect(completion).to be(1.0)
+ end
+
+ it "returns 100% completion when there are no categories" do
+ SiteSetting.ai_translation_backfill_limit_to_public_content = false
+ Category.destroy_all
+
+ completion = DiscourseAi::Translation::CategoryCandidates.get_completion_per_locale("pt")
+ expect(completion).to eq(1.0)
+ end
+ end
+end
diff --git a/plugins/discourse-ai/spec/lib/translation/post_candidates_spec.rb b/plugins/discourse-ai/spec/lib/translation/post_candidates_spec.rb
new file mode 100644
index 0000000000000..47042bae4292f
--- /dev/null
+++ b/plugins/discourse-ai/spec/lib/translation/post_candidates_spec.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+describe DiscourseAi::Translation::PostCandidates do
+ describe ".get" do
+ it "does not return bot posts" do
+ post = Fabricate(:post, user: Discourse.system_user)
+
+ expect(DiscourseAi::Translation::PostCandidates.get).not_to include(post)
+ end
+
+ it "does not return posts older than ai_translation_backfill_max_age_days" do
+ post =
+ Fabricate(
+ :post,
+ created_at: SiteSetting.ai_translation_backfill_max_age_days.days.ago - 1.day,
+ )
+
+ expect(DiscourseAi::Translation::PostCandidates.get).not_to include(post)
+ end
+
+ it "does not return deleted posts" do
+ post = Fabricate(:post, deleted_at: Time.now)
+
+ expect(DiscourseAi::Translation::PostCandidates.get).not_to include(post)
+ end
+
+ describe "SiteSetting.ai_translation_backfill_limit_to_public_content" do
+ fab!(:pm_post) { Fabricate(:post, topic: Fabricate(:private_message_topic)) }
+ fab!(:group_pm_post) do
+ Fabricate(
+ :post,
+ topic: Fabricate(:private_message_topic, allowed_groups: [Fabricate(:group)]),
+ )
+ end
+ fab!(:public_post) do
+ Fabricate(
+ :post,
+ topic: Fabricate(:topic, category: Fabricate(:category, read_restricted: false)),
+ )
+ end
+
+ it "excludes PMs and only includes posts from public categories" do
+ SiteSetting.ai_translation_backfill_limit_to_public_content = true
+
+ posts = DiscourseAi::Translation::PostCandidates.get
+ expect(posts).not_to include(pm_post)
+ expect(posts).not_to include(group_pm_post)
+ expect(posts).to include(public_post)
+ end
+
+ it "includes all regular posts and group PMs but not personal PMs" do
+ SiteSetting.ai_translation_backfill_limit_to_public_content = false
+
+ posts = DiscourseAi::Translation::PostCandidates.get
+ expect(posts).not_to include(pm_post)
+ expect(posts).to include(group_pm_post)
+ expect(posts).to include(public_post)
+ end
+ end
+ end
+
+ describe ".get_completion_per_locale" do
+ context "when (scenario A) percentage determined by post's locale" do
+ it "returns 100% completion if all posts are in the locale" do
+ locale = "pt_BR"
+ Fabricate(:post, locale:)
+ Post.update_all(locale: locale)
+ Fabricate(:post, locale: "pt")
+
+ completion = DiscourseAi::Translation::PostCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1.0)
+ end
+
+ it "returns X% completion if some posts are in the locale" do
+ locale = "es"
+ Fabricate(:post, locale:)
+ Fabricate(:post, locale: "not_es")
+
+ completion = DiscourseAi::Translation::PostCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1 / Post.count.to_f)
+ end
+ end
+
+ context "when (scenario B) percentage determined by post localizations" do
+ it "returns 100% completion if all posts have a localization in the locale" do
+ locale = "pt_BR"
+ Fabricate(:post)
+ Post.all.each { |post| Fabricate(:post_localization, post:, locale:) }
+ Fabricate(:post_localization, locale: "pt")
+
+ completion = DiscourseAi::Translation::PostCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1.0)
+ end
+
+ it "returns X% completion if some posts have a localization in the locale" do
+ locale = "es"
+ Fabricate(:post_localization, locale:)
+ Fabricate(:post_localization, locale: "not_es")
+
+ completion = DiscourseAi::Translation::PostCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1 / Post.count.to_f)
+ end
+ end
+
+ it "returns the correct percentage based on (scenario A & B) `post.locale` and `PostLocalization` in the specified locale" do
+ locale = "es"
+
+ # translated candidates
+ Fabricate(:post, locale:)
+ post2 = Fabricate(:post)
+ Fabricate(:post_localization, post: post2, locale:)
+
+ # untranslated candidate
+ post4 = Fabricate(:post)
+ Fabricate(:post_localization, post: post4, locale: "zh_CN")
+
+ # not a candidate as it is a bot post
+ post3 = Fabricate(:post, user: Discourse.system_user)
+ Fabricate(:post_localization, post: post3, locale:)
+
+ completion = DiscourseAi::Translation::PostCandidates.get_completion_per_locale(locale)
+ translated_candidates = 2 # post1 + post2
+ total_candidates = Post.count - 1 # excluding the bot post
+ expect(completion).to eq(translated_candidates / total_candidates.to_f)
+ end
+
+ it "does not exceed 100% completion when post.locale and post_localization both exist" do
+ locale = "es"
+ post = Fabricate(:post, locale:)
+ Fabricate(:post_localization, post:, locale:)
+
+ completion = DiscourseAi::Translation::PostCandidates.get_completion_per_locale(locale)
+ expect(completion).to be(1.0)
+ end
+
+ it "returns 100% completion when no posts are present" do
+ SiteSetting.ai_translation_backfill_max_age_days = 0
+
+ completion = DiscourseAi::Translation::PostCandidates.get_completion_per_locale("es")
+ expect(completion).to eq(1.0)
+ end
+ end
+end
diff --git a/plugins/discourse-ai/spec/lib/translation/topic_candidates_spec.rb b/plugins/discourse-ai/spec/lib/translation/topic_candidates_spec.rb
new file mode 100644
index 0000000000000..bc3b6bdb54ae3
--- /dev/null
+++ b/plugins/discourse-ai/spec/lib/translation/topic_candidates_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+describe DiscourseAi::Translation::TopicCandidates do
+ describe ".get" do
+ it "does not return bot topics" do
+ topic = Fabricate(:topic, user: Discourse.system_user)
+
+ expect(DiscourseAi::Translation::TopicCandidates.get).not_to include(topic)
+ end
+
+ it "does not return topics older than ai_translation_backfill_max_age_days" do
+ topic =
+ Fabricate(
+ :topic,
+ created_at: SiteSetting.ai_translation_backfill_max_age_days.days.ago - 1.day,
+ )
+
+ expect(DiscourseAi::Translation::TopicCandidates.get).not_to include(topic)
+ end
+
+ it "does not return deleted topics" do
+ topic = Fabricate(:topic, deleted_at: Time.now)
+
+ expect(DiscourseAi::Translation::TopicCandidates.get).not_to include(topic)
+ end
+
+ describe "SiteSetting.ai_translation_backfill_limit_to_public_content" do
+ fab!(:pm) { Fabricate(:private_message_topic) }
+ fab!(:group_pm) { Fabricate(:private_message_topic, allowed_groups: [Fabricate(:group)]) }
+ fab!(:public_topic) do
+ Fabricate(:topic, category: Fabricate(:category, read_restricted: false))
+ end
+
+ it "excludes PMs and only includes topics from public categories" do
+ SiteSetting.ai_translation_backfill_limit_to_public_content = true
+
+ topics = DiscourseAi::Translation::TopicCandidates.get
+ expect(topics).not_to include(pm)
+ expect(topics).not_to include(group_pm)
+ expect(topics).to include(public_topic)
+ end
+
+ it "includes all regular topics and group PMs but not personal PMs" do
+ SiteSetting.ai_translation_backfill_limit_to_public_content = false
+
+ topics = DiscourseAi::Translation::TopicCandidates.get
+ expect(topics).not_to include(pm)
+ expect(topics).to include(group_pm)
+ expect(topics).to include(public_topic)
+ end
+ end
+ end
+
+ describe ".get_completion_per_locale" do
+ context "when (scenario A) percentage determined by topic's locale" do
+ it "returns 100% completion if all topics are in the locale" do
+ locale = "pt_BR"
+ Fabricate(:topic, locale:)
+ Topic.update_all(locale: locale)
+ Fabricate(:topic, locale: "pt")
+
+ completion = DiscourseAi::Translation::TopicCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1.0)
+ end
+
+ it "returns X% completion if some topics are in the locale" do
+ locale = "es"
+ Fabricate(:topic, locale:)
+ Fabricate(:topic, locale: "not_es")
+
+ completion = DiscourseAi::Translation::TopicCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1 / Topic.count.to_f)
+ end
+ end
+
+ context "when (scenario B) percentage determined by topic localizations" do
+ it "returns 100% completion if all topics have a localization in the locale" do
+ locale = "pt_BR"
+ Fabricate(:topic)
+ Topic.all.each { |topic| Fabricate(:topic_localization, topic:, locale:) }
+ Fabricate(:topic_localization, locale: "pt")
+
+ completion = DiscourseAi::Translation::TopicCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1.0)
+ end
+
+ it "returns X% completion if some topics have a localization in the locale" do
+ locale = "es"
+ Fabricate(:topic_localization, locale:)
+ Fabricate(:topic_localization, locale: "not_es")
+
+ completion = DiscourseAi::Translation::TopicCandidates.get_completion_per_locale(locale)
+ expect(completion).to eq(1 / Topic.count.to_f)
+ end
+ end
+
+ it "returns the correct percentage based on (scenario A & B) `topic.locale` and `TopicLocalization` in the specified locale" do
+ locale = "es"
+
+ # translated candidates
+ Fabricate(:topic, locale:)
+ topic2 = Fabricate(:topic)
+ Fabricate(:topic_localization, topic: topic2, locale:)
+
+ # untranslated candidate
+ topic4 = Fabricate(:topic)
+ Fabricate(:topic_localization, topic: topic4, locale: "zh_CN")
+
+ # not a candidate as it is a bot topic
+ topic3 = Fabricate(:topic, user: Discourse.system_user)
+ Fabricate(:topic_localization, topic: topic3, locale:)
+
+ completion = DiscourseAi::Translation::TopicCandidates.get_completion_per_locale(locale)
+ translated_candidates = 2 # topic1 + topic2
+ total_candidates = Topic.count - 1 # excluding the bot topic
+ expect(completion).to eq(translated_candidates / total_candidates.to_f)
+ end
+
+ it "does not exceed 100% completion when topic.locale and topic_localization both exist" do
+ locale = "es"
+ topic = Fabricate(:topic, locale:)
+ Fabricate(:topic_localization, topic:, locale:)
+
+ completion = DiscourseAi::Translation::TopicCandidates.get_completion_per_locale(locale)
+ expect(completion).to be(1.0)
+ end
+
+ it "returns 100% if no topics" do
+ SiteSetting.ai_translation_backfill_max_age_days = 0
+ SiteSetting.ai_translation_backfill_limit_to_public_content = false
+
+ completion = DiscourseAi::Translation::TopicCandidates.get_completion_per_locale("es")
+ expect(completion).to eq(1.0)
+ end
+ end
+end
diff --git a/plugins/discourse-ai/spec/requests/admin/ai_usage_controller_spec.rb b/plugins/discourse-ai/spec/requests/admin/ai_usage_controller_spec.rb
index a1902a419a4f2..01981a64fff73 100644
--- a/plugins/discourse-ai/spec/requests/admin/ai_usage_controller_spec.rb
+++ b/plugins/discourse-ai/spec/requests/admin/ai_usage_controller_spec.rb
@@ -154,6 +154,46 @@
expect(data_by_hour.first[1]["total_tokens"]).to eq(150)
end
end
+
+ context "with different timezones" do
+ before { freeze_time Time.parse("2024-07-28 00:30:00 UTC") }
+
+ let(:base_time) { Time.parse("2024-07-28 00:30:00 UTC") } # 8:30 AM Singapore
+ let(:singapore_tz) { "Asia/Singapore" }
+
+ let!(:log_sg1) do
+ AiApiAuditLog.create!(
+ provider_id: 1,
+ feature_name: "summarize",
+ language_model: "gpt-4",
+ request_tokens: 1000,
+ response_tokens: 50,
+ created_at: base_time,
+ )
+ end
+
+ let!(:log_sg2) do
+ AiApiAuditLog.create!(
+ provider_id: 1,
+ feature_name: "summarize",
+ language_model: "gpt-4",
+ request_tokens: 1000,
+ response_tokens: 50,
+ created_at: base_time - 1.hour,
+ )
+ end
+
+ it "shows correct data across timezone boundaries" do
+ report =
+ DiscourseAi::Completions::Report.new(
+ start_date: base_time.in_time_zone(singapore_tz).beginning_of_day,
+ end_date: base_time.in_time_zone(singapore_tz).end_of_day,
+ timezone: singapore_tz,
+ )
+
+ expect(report.tokens_by_period(:hour).count).to eq(2)
+ end
+ end
end
context "when not admin" do
diff --git a/plugins/discourse-ai/spec/system/ai_bot/homepage_spec.rb b/plugins/discourse-ai/spec/system/ai_bot/homepage_spec.rb
index 7380e8b8c2ce7..8d2b06dc50d47 100644
--- a/plugins/discourse-ai/spec/system/ai_bot/homepage_spec.rb
+++ b/plugins/discourse-ai/spec/system/ai_bot/homepage_spec.rb
@@ -252,15 +252,20 @@
llm_name = CGI.escape(claude_2_dup.display_name)
visit "/discourse-ai/ai-bot/conversations?persona=#{persona_name}&llm=#{llm_name}"
+ ai_pm_homepage.persona_selector.expand # not needed, but helps to see what the list has
expect(ai_pm_homepage.persona_selector).to have_selected_name(persona.name)
expect(ai_pm_homepage.llm_selector).to have_selected_name(claude_2_dup.display_name)
end
it "removes persona from selector when allow_personal_messages is disabled" do
- persona.update!(allow_personal_messages: false)
- ai_pm_homepage.visit
- ai_pm_homepage.persona_selector.expand
- expect(ai_pm_homepage.persona_selector).to have_no_option_name(persona.name)
+ begin
+ persona.update!(allow_personal_messages: false)
+ ai_pm_homepage.visit
+ ai_pm_homepage.persona_selector.expand
+ expect(ai_pm_homepage.persona_selector).to have_no_option_name(persona.name)
+ ensure
+ persona.update!(allow_personal_messages: true)
+ end
end
it "includes persona in selector when allow_personal_messages is enabled" do
diff --git a/plugins/discourse-rss-polling/app/jobs/jobs/discourse_rss_polling/fix_topic_embed_authors.rb b/plugins/discourse-rss-polling/app/jobs/jobs/discourse_rss_polling/fix_topic_embed_authors.rb
deleted file mode 100644
index 16fc7f040658b..0000000000000
--- a/plugins/discourse-rss-polling/app/jobs/jobs/discourse_rss_polling/fix_topic_embed_authors.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module Jobs
- module DiscourseRssPolling
- class FixTopicEmbedAuthors < ::Jobs::Base
- sidekiq_options queue: "low"
-
- def mismatched_topic_embeds
- TopicEmbed.joins(post: :topic).where("posts.user_id != topics.user_id")
- end
-
- def execute(args)
- mismatched_topic_embeds.find_each do |topic_embed|
- post = topic_embed.post
-
- PostOwnerChanger.new(
- post_ids: [post.id],
- topic_id: post.topic_id,
- new_owner: post.user,
- acting_user: Discourse.system_user,
- skip_revision: true,
- ).change_owner!
- end
- end
- end
- end
-end
diff --git a/plugins/discourse-rss-polling/db/migrate/20180828095129_push_fix_topic_embed_authors_job.rb b/plugins/discourse-rss-polling/db/migrate/20180828095129_push_fix_topic_embed_authors_job.rb
deleted file mode 100644
index a1ca719a56747..0000000000000
--- a/plugins/discourse-rss-polling/db/migrate/20180828095129_push_fix_topic_embed_authors_job.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class PushFixTopicEmbedAuthorsJob < ActiveRecord::Migration[5.2]
- def change
- Jobs.enqueue("DiscourseRssPolling::FixTopicEmbedAuthors")
- end
-end
diff --git a/plugins/discourse-rss-polling/spec/jobs/fix_topic_embed_authors_spec.rb b/plugins/discourse-rss-polling/spec/jobs/fix_topic_embed_authors_spec.rb
deleted file mode 100644
index 245e32906579f..0000000000000
--- a/plugins/discourse-rss-polling/spec/jobs/fix_topic_embed_authors_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Jobs::DiscourseRssPolling::FixTopicEmbedAuthors do
- let(:job) { Jobs::DiscourseRssPolling::FixTopicEmbedAuthors.new }
-
- describe "#execute" do
- before { Jobs.run_later! }
-
- it "makes sure the topic and first post have the same author" do
- Sidekiq::Testing.fake! do
- topic_embed = create_mismatched_topic_embed
- post = topic_embed.post
- expected_user = post.user
-
- expect(post.user).to_not eq(post.topic.user)
-
- job.execute({})
-
- post.reload
- expect(post.user).to eq(expected_user)
- expect(post.user).to eq(post.topic.user)
- end
- end
-
- def create_mismatched_topic_embed
- topic_embed = Fabricate(:topic_embed, embed_url: "http://example.com/post/248")
- new_user = Fabricate(:user)
-
- topic_embed.post.revise(
- Discourse.system_user,
- { user_id: new_user.id },
- skip_validations: true,
- bypass_rate_limiter: true,
- )
-
- topic_embed
- end
- end
-end
diff --git a/plugins/footnote/assets/javascripts/api-initializers/inline-footnotes.gjs b/plugins/footnote/assets/javascripts/api-initializers/inline-footnotes.gjs
new file mode 100644
index 0000000000000..d7e6cb49668dd
--- /dev/null
+++ b/plugins/footnote/assets/javascripts/api-initializers/inline-footnotes.gjs
@@ -0,0 +1,64 @@
+import { htmlSafe } from "@ember/template";
+import { apiInitializer } from "discourse/lib/api";
+import DTooltipInstance from "float-kit/lib/d-tooltip-instance";
+
+const TooltipContentComponent =
+ {{htmlSafe @data.contentHtml}}
+;
+
+export default apiInitializer((api) => {
+ function onFootnoteClick(event) {
+ event.preventDefault();
+
+ const tooltipService = api.container.lookup("service:tooltip");
+
+ const instance = new DTooltipInstance(api.container, {
+ identifier: "inline-footnote",
+ interactive: true,
+ closeOnScroll: false,
+ closeOnClickOutside: true,
+ component: TooltipContentComponent,
+ data: {
+ contentHtml: event.target.dataset.footnoteContent,
+ },
+ });
+ instance.trigger = event.target;
+ instance.detachedTrigger = true;
+
+ tooltipService.show(instance);
+ }
+
+ api.decorateCookedElement((elem) => {
+ if (
+ !api.container.lookup("service:site-settings").display_footnotes_inline
+ ) {
+ return;
+ }
+
+ const footnoteRefs = elem.querySelectorAll("sup.footnote-ref");
+
+ footnoteRefs.forEach((footnoteRef) => {
+ const refLink = footnoteRef.querySelector("a");
+ if (!refLink) {
+ return;
+ }
+
+ const footnoteId = refLink.getAttribute("href");
+ const footnoteContent = elem.querySelector(footnoteId)?.innerHTML;
+
+ const expandableFootnote = document.createElement("a");
+ expandableFootnote.classList.add("expand-footnote");
+ expandableFootnote.href = "";
+ expandableFootnote.role = "button";
+ expandableFootnote.dataset.footnoteId = footnoteId;
+ expandableFootnote.dataset.footnoteContent = footnoteContent;
+ expandableFootnote.addEventListener("click", onFootnoteClick);
+
+ footnoteRef.after(expandableFootnote);
+ });
+
+ if (footnoteRefs.length) {
+ elem.classList.add("inline-footnotes");
+ }
+ });
+});
diff --git a/plugins/footnote/assets/javascripts/initializers/inline-footnotes.js b/plugins/footnote/assets/javascripts/initializers/inline-footnotes.js
deleted file mode 100644
index 934ad5d581535..0000000000000
--- a/plugins/footnote/assets/javascripts/initializers/inline-footnotes.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import { createPopper } from "@popperjs/core";
-import { iconHTML } from "discourse/lib/icon-library";
-import { withPluginApi } from "discourse/lib/plugin-api";
-
-let inlineFootnotePopper;
-
-function applyInlineFootnotes(elem) {
- const footnoteRefs = elem.querySelectorAll("sup.footnote-ref");
-
- footnoteRefs.forEach((footnoteRef) => {
- const refLink = footnoteRef.querySelector("a");
- if (!refLink) {
- return;
- }
-
- const expandableFootnote = document.createElement("a");
- expandableFootnote.classList.add("expand-footnote");
- expandableFootnote.innerHTML = iconHTML("ellipsis");
- expandableFootnote.href = "";
- expandableFootnote.role = "button";
- expandableFootnote.dataset.footnoteId = refLink.getAttribute("href");
-
- footnoteRef.after(expandableFootnote);
- });
-
- if (footnoteRefs.length) {
- elem.classList.add("inline-footnotes");
- }
-}
-
-function buildTooltip() {
- const template = document.createElement("template");
- template.innerHTML = `
-
- `.trim();
-
- return template.content.firstChild;
-}
-
-function footnoteEventHandler(event) {
- const tooltip = document.getElementById("footnote-tooltip");
- const displayedFootnoteId = tooltip?.dataset.footnoteId;
- const expandableFootnote = event.target;
- const footnoteId = expandableFootnote.dataset.footnoteId;
-
- inlineFootnotePopper?.destroy();
- tooltip?.removeAttribute("data-show");
- tooltip?.removeAttribute("data-footnote-id");
-
- if (!event.target.classList.contains("expand-footnote")) {
- // dismissing the tooltip by clicking outside
- return;
- }
-
- event.preventDefault();
- event.stopPropagation();
-
- if (displayedFootnoteId === footnoteId) {
- // dismissing the tooltip by clicking the footnote button
- return;
- }
-
- // append footnote to tooltip body
- const footnoteContent = tooltip.querySelector(".footnote-tooltip-content");
- let cooked = expandableFootnote.closest(".cooked");
- if (cooked.dataset.refPostId != null) {
- // For full screen tables, redirect
- cooked = document.querySelector(
- `article[data-post-id="${cooked.dataset.refPostId}"] .cooked`
- );
- }
- const newContent = cooked.querySelector(footnoteId);
- footnoteContent.innerHTML = newContent.innerHTML;
-
- // display tooltip
- tooltip.dataset.show = "";
- tooltip.dataset.footnoteId = footnoteId;
-
- // setup popper
- inlineFootnotePopper?.destroy();
- inlineFootnotePopper = createPopper(expandableFootnote, tooltip, {
- modifiers: [
- {
- name: "arrow",
- options: { element: tooltip.querySelector("#arrow") },
- },
- {
- name: "preventOverflow",
- options: {
- altAxis: true,
- padding: 5,
- },
- },
- {
- name: "offset",
- options: {
- offset: [0, 12],
- },
- },
- ],
- });
-}
-
-export default {
- name: "inline-footnotes",
-
- initialize(container) {
- if (!container.lookup("service:site-settings").display_footnotes_inline) {
- return;
- }
-
- document.body.append(buildTooltip());
- window.addEventListener("click", footnoteEventHandler, true);
-
- withPluginApi((api) => {
- api.decorateCookedElement((elem) => applyInlineFootnotes(elem), {
- onlyStream: true,
- id: "inline-footnotes",
- });
-
- api.onPageChange(() => {
- inlineFootnotePopper?.destroy();
-
- const tooltip = document.getElementById("footnote-tooltip");
- tooltip?.removeAttribute("data-show");
- tooltip?.removeAttribute("data-footnote-id");
- });
- });
- },
-
- teardown() {
- inlineFootnotePopper?.destroy();
- window.removeEventListener("click", footnoteEventHandler);
- document.getElementById("footnote-tooltip")?.remove();
- },
-};
diff --git a/plugins/footnote/assets/stylesheets/footnotes.scss b/plugins/footnote/assets/stylesheets/footnotes.scss
index 3c07028e95287..ea5c3739e8680 100644
--- a/plugins/footnote/assets/stylesheets/footnotes.scss
+++ b/plugins/footnote/assets/stylesheets/footnotes.scss
@@ -1,21 +1,18 @@
.inline-footnotes {
+ counter-reset: inline-footnote;
+
a.expand-footnote {
user-select: none;
- padding: 0 0.4em;
- color: var(--primary-low-mid-or-secondary-high);
- background: var(--primary-low);
- border-radius: 3px;
- min-height: 1.25em;
- display: inline-flex;
- align-items: center;
-
- &:hover {
- background: var(--primary-medium);
- color: var(--secondary);
- }
+ display: inline-block;
- > * {
- pointer-events: none;
+ &::after {
+ padding: 0 0.125em;
+ display: inline-block;
+ content: "[" counter(inline-footnote) "]";
+ vertical-align: super;
+ font-size: 0.75rem;
+ line-height: 1;
+ counter-increment: inline-footnote;
}
}
@@ -38,78 +35,22 @@
}
}
-#footnote-tooltip {
- background-color: var(--primary-low);
- color: var(--primary);
- padding: 0.5em;
- font-size: var(--font-down-1);
- border-radius: 3px;
- display: none;
- z-index: z("modal", "tooltip");
- max-width: 400px;
+.fk-d-tooltip__content[data-identifier="inline-footnote"] {
overflow-wrap: break-word;
- box-sizing: border-box;
+ font-size: var(--font-down-1);
- .mobile-view & {
- // tooltips are positioned 5px from the left
- // - 10px accounts for this and gives 5px space on the right
- max-width: calc(100dvw - 10px);
+ .footnote-backref {
+ display: none;
}
- .footnote-tooltip-content {
- overflow: hidden;
-
- .footnote-backref {
- display: none;
- }
-
- img {
- object-fit: cover;
- max-width: 100%;
- }
-
- p {
- margin: 0;
- }
+ img {
+ object-fit: cover;
+ max-width: 100%;
}
-}
-#footnote-tooltip[data-show] {
- display: block;
-}
-
-#arrow,
-#arrow::before {
- position: absolute;
- width: 10px;
- height: 10px;
- background: inherit;
-}
-
-#arrow {
- visibility: hidden;
-}
-
-#arrow::before {
- visibility: visible;
- content: "";
- transform: rotate(45deg);
-}
-
-#footnote-tooltip[data-popper-placement^="top"] > #arrow {
- bottom: -4px;
-}
-
-#footnote-tooltip[data-popper-placement^="bottom"] > #arrow {
- top: -4px;
-}
-
-#footnote-tooltip[data-popper-placement^="left"] > #arrow {
- right: -4px;
-}
-
-#footnote-tooltip[data-popper-placement^="right"] > #arrow {
- left: -4px;
+ p {
+ margin: 0;
+ }
}
.ProseMirror {
diff --git a/plugins/footnote/test/javascripts/acceptance/footnote-test.js b/plugins/footnote/test/javascripts/acceptance/footnote-test.js
index 9af93cead27f6..8867b7083e674 100644
--- a/plugins/footnote/test/javascripts/acceptance/footnote-test.js
+++ b/plugins/footnote/test/javascripts/acceptance/footnote-test.js
@@ -1,9 +1,12 @@
-import { click, visit } from "@ember/test-helpers";
+import { click, triggerEvent, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { cloneJSON } from "discourse/lib/object";
import topicFixtures from "discourse/tests/fixtures/topic";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
+const TOOLTIP_SELECTOR =
+ ".fk-d-tooltip__content[data-identifier='inline-footnote']";
+
acceptance("Discourse Footnote Plugin", function (needs) {
needs.settings({
display_footnotes_inline: true,
@@ -29,44 +32,24 @@ acceptance("Discourse Footnote Plugin", function (needs) {
test("displays the footnote on click", async function (assert) {
await visit("/t/-/45");
- assert.dom("#footnote-tooltip", document.body).exists();
-
// open
await click(".expand-footnote");
- assert
- .dom(".footnote-tooltip-content", document.body)
- .hasText("consectetur adipiscing elit ↩︎");
- assert.dom("#footnote-tooltip", document.body).hasAttribute("data-show");
+
+ assert.dom(TOOLTIP_SELECTOR).hasText("consectetur adipiscing elit ↩︎");
// close by clicking outside
- await click(document.body);
- assert
- .dom("#footnote-tooltip", document.body)
- .doesNotHaveAttribute("data-show");
+ await triggerEvent(".d-header", "pointerdown");
+ assert.dom(TOOLTIP_SELECTOR).doesNotExist();
// open again
await click(".expand-footnote");
- assert
- .dom(".footnote-tooltip-content", document.body)
- .hasText("consectetur adipiscing elit ↩︎");
- assert.dom("#footnote-tooltip", document.body).hasAttribute("data-show");
-
- // close by clicking the button
- await click(".expand-footnote");
- assert
- .dom("#footnote-tooltip", document.body)
- .doesNotHaveAttribute("data-show");
+ assert.dom(TOOLTIP_SELECTOR).hasText("consectetur adipiscing elit ↩︎");
});
test("clicking a second footnote with same name works", async function (assert) {
await visit("/t/-/45");
- assert.dom("#footnote-tooltip", document.body).exists();
-
await click(".second .expand-footnote");
- assert
- .dom(".footnote-tooltip-content", document.body)
- .hasText("consectetur adipiscing elit ↩︎");
- assert.dom("#footnote-tooltip", document.body).hasAttribute("data-show");
+ assert.dom(TOOLTIP_SELECTOR).hasText("consectetur adipiscing elit ↩︎");
});
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index edb6c1b653a9e..766da7ffa3264 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -56,8 +56,8 @@ importers:
specifier: ^3.0.0
version: 3.0.0(prettier@3.5.3)
'@swc/core':
- specifier: ^1.13.2
- version: 1.13.2
+ specifier: ^1.13.3
+ version: 1.13.3
chart.js:
specifier: 3.5.1
version: 3.5.1
@@ -151,7 +151,7 @@ importers:
version: 2.2.0
'@embroider/test-setup':
specifier: ^4.0.0
- version: 4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
'@glimmer/component':
specifier: ^1.1.2
version: 1.1.2(@babel/core@7.28.0)
@@ -168,8 +168,8 @@ importers:
specifier: ^3.0.0
version: 3.0.0
ember-cli:
- specifier: ~6.5.0
- version: 6.5.0(handlebars@4.7.8)(underscore@1.13.7)
+ specifier: ~6.6.0
+ version: 6.6.0(handlebars@4.7.8)(underscore@1.13.7)
ember-cli-inject-live-reload:
specifier: ^2.1.0
version: 2.1.0
@@ -184,13 +184,13 @@ importers:
version: 1.1.3
ember-load-initializers:
specifier: ^3.0.1
- version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
ember-resolver:
specifier: ^13.1.1
version: 13.1.1
ember-source:
specifier: ~5.12.0
- version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-source-channel-url:
specifier: ^3.0.0
version: 3.0.0(encoding@0.1.13)
@@ -199,7 +199,7 @@ importers:
version: 4.7.0
webpack:
specifier: ^5.101.0
- version: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ version: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
app/assets/javascripts/custom-proxy:
devDependencies:
@@ -231,7 +231,7 @@ importers:
version: 8.1.4
ember-auto-import:
specifier: ^2.10.0
- version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-cli-babel:
specifier: ^8.2.0
version: 8.2.0(@babel/core@7.28.0)
@@ -256,7 +256,7 @@ importers:
version: 4.0.9
webpack:
specifier: ^5.101.0
- version: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ version: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
app/assets/javascripts/discourse:
dependencies:
@@ -398,13 +398,13 @@ importers:
version: 0.9.1(patch_hash=s67qh4jsmpbr3llstdi3a5zeze)
'@ember/legacy-built-in-components':
specifier: ^0.5.0
- version: 0.5.0(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 0.5.0(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
'@ember/optional-features':
specifier: ^2.2.0
version: 2.2.0
'@ember/render-modifiers':
specifier: ^3.0.0
- version: 3.0.0(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 3.0.0(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
'@ember/string':
specifier: ^4.0.1
version: 4.0.1
@@ -428,7 +428,7 @@ importers:
version: 2.1.8(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))
'@embroider/webpack':
specifier: ^4.1.0
- version: 4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
'@floating-ui/dom':
specifier: ^1.7.2
version: 1.7.2
@@ -442,8 +442,8 @@ importers:
specifier: ^2.11.8
version: 2.11.8
'@swc/core':
- specifier: ^1.13.2
- version: 1.13.2
+ specifier: ^1.13.3
+ version: 1.13.3
'@types/jquery':
specifier: ^3.5.32
version: 3.5.32
@@ -512,25 +512,25 @@ importers:
version: 2.0.1(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510)
ember-auto-import:
specifier: ^2.10.0
- version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-buffered-proxy:
specifier: ^2.1.1
version: 2.1.1(@babel/core@7.28.0)
ember-cached-decorator-polyfill:
specifier: ^1.0.2
- version: 1.0.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 1.0.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
ember-cli:
- specifier: ~6.5.0
- version: 6.5.0(handlebars@4.7.8)(underscore@1.13.7)
+ specifier: ~6.6.0
+ version: 6.6.0(handlebars@4.7.8)(underscore@1.13.7)
ember-cli-app-version:
specifier: ^7.0.0
- version: 7.0.0(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 7.0.0(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
ember-cli-babel:
specifier: ^8.2.0
version: 8.2.0(@babel/core@7.28.0)
ember-cli-deprecation-workflow:
specifier: ^3.4.0
- version: 3.4.0(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 3.4.0(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
ember-cli-htmlbars:
specifier: ^6.3.0
version: 6.3.0
@@ -551,10 +551,10 @@ importers:
version: 6.1.1
ember-exam:
specifier: ^9.1.0
- version: 9.1.0(@glint/template@1.4.1-unstable.34c4510)(ember-qunit@9.0.3(@ember/test-helpers@5.2.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510)(qunit@2.24.1))(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))(qunit@2.24.1)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 9.1.0(@glint/template@1.4.1-unstable.34c4510)(ember-qunit@9.0.3(@ember/test-helpers@5.2.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510)(qunit@2.24.1))(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))(qunit@2.24.1)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-load-initializers:
specifier: ^3.0.1
- version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
ember-modifier:
specifier: ^4.2.2
version: 4.2.2(@babel/core@7.28.0)
@@ -563,7 +563,7 @@ importers:
version: 9.0.3(@ember/test-helpers@5.2.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510)(qunit@2.24.1)
ember-source:
specifier: ~5.12.0
- version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-template-imports:
specifier: ^4.3.0
version: 4.3.0
@@ -578,7 +578,7 @@ importers:
version: 2.6.0
imports-loader:
specifier: ^5.0.0
- version: 5.0.0(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 5.0.0(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
jquery:
specifier: ^3.7.1
version: 3.7.1
@@ -632,10 +632,10 @@ importers:
version: 2.1.1(patch_hash=ng672yys7q7cl7vz44xn3y54uq)
webpack:
specifier: ^5.101.0
- version: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ version: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
webpack-retry-chunk-load-plugin:
specifier: ^3.1.1
- version: 3.1.1(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 3.1.1(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
webpack-stats-plugin:
specifier: ^1.1.3
version: 1.1.3
@@ -662,7 +662,7 @@ importers:
version: link:../discourse-i18n
ember-auto-import:
specifier: ^2.10.0
- version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
markdown-it:
specifier: 14.0.0
version: 14.0.0
@@ -698,14 +698,14 @@ importers:
version: 4.3.0
ember-this-fallback:
specifier: ^0.4.0
- version: 0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
devDependencies:
ember-cli:
- specifier: ~6.5.0
- version: 6.5.0(handlebars@4.7.8)(underscore@1.13.7)
+ specifier: ~6.6.0
+ version: 6.6.0(handlebars@4.7.8)(underscore@1.13.7)
webpack:
specifier: ^5.101.0
- version: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ version: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
app/assets/javascripts/discourse-widget-hbs:
dependencies:
@@ -714,7 +714,7 @@ importers:
version: 7.28.0(supports-color@8.1.1)
ember-auto-import:
specifier: ^2.10.0
- version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-cli-babel:
specifier: ^8.2.0
version: 8.2.0(@babel/core@7.28.0)
@@ -727,7 +727,7 @@ importers:
version: 2.2.0
'@embroider/test-setup':
specifier: ^4.0.0
- version: 4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
'@glimmer/component':
specifier: ^1.1.2
version: 1.1.2(@babel/core@7.28.0)
@@ -738,8 +738,8 @@ importers:
specifier: ^3.0.0
version: 3.0.0
ember-cli:
- specifier: ~6.5.0
- version: 6.5.0(handlebars@4.7.8)(underscore@1.13.7)
+ specifier: ~6.6.0
+ version: 6.6.0(handlebars@4.7.8)(underscore@1.13.7)
ember-cli-inject-live-reload:
specifier: ^2.1.0
version: 2.1.0
@@ -754,13 +754,13 @@ importers:
version: 1.1.3
ember-load-initializers:
specifier: ^3.0.1
- version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
ember-resolver:
specifier: ^13.1.1
version: 13.1.1
ember-source:
specifier: ~5.12.0
- version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-source-channel-url:
specifier: ^3.0.0
version: 3.0.0(encoding@0.1.13)
@@ -769,7 +769,7 @@ importers:
version: 4.7.0
webpack:
specifier: ^5.101.0
- version: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ version: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
app/assets/javascripts/ember-cli-progress-ci: {}
@@ -780,13 +780,13 @@ importers:
version: 7.28.0(supports-color@8.1.1)
'@ember/render-modifiers':
specifier: ^3.0.0
- version: 3.0.0(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 3.0.0(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
'@floating-ui/dom':
specifier: ^1.7.2
version: 1.7.2
ember-auto-import:
specifier: ^2.10.0
- version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-cli-babel:
specifier: ^8.2.0
version: 8.2.0(@babel/core@7.28.0)
@@ -808,7 +808,7 @@ importers:
version: 2.2.0
'@embroider/test-setup':
specifier: ^4.0.0
- version: 4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
'@glimmer/component':
specifier: ^1.1.2
version: 1.1.2(@babel/core@7.28.0)
@@ -825,8 +825,8 @@ importers:
specifier: ^3.0.0
version: 3.0.0
ember-cli:
- specifier: ~6.5.0
- version: 6.5.0(handlebars@4.7.8)(underscore@1.13.7)
+ specifier: ~6.6.0
+ version: 6.6.0(handlebars@4.7.8)(underscore@1.13.7)
ember-cli-inject-live-reload:
specifier: ^2.1.0
version: 2.1.0
@@ -841,13 +841,13 @@ importers:
version: 1.1.3
ember-load-initializers:
specifier: ^3.0.1
- version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
ember-resolver:
specifier: ^13.1.1
version: 13.1.1
ember-source:
specifier: ~5.12.0
- version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-source-channel-url:
specifier: ^3.0.0
version: 3.0.0(encoding@0.1.13)
@@ -856,7 +856,7 @@ importers:
version: 4.7.0
webpack:
specifier: ^5.101.0
- version: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ version: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
app/assets/javascripts/pretty-text:
dependencies:
@@ -868,7 +868,7 @@ importers:
version: link:../discourse-i18n
ember-auto-import:
specifier: ^2.10.0
- version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-cli-babel:
specifier: ^8.2.0
version: 8.2.0(@babel/core@7.28.0)
@@ -884,7 +884,7 @@ importers:
version: 2.2.0
'@embroider/test-setup':
specifier: ^4.0.0
- version: 4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
'@glimmer/component':
specifier: ^1.1.2
version: 1.1.2(@babel/core@7.28.0)
@@ -901,8 +901,8 @@ importers:
specifier: ^3.0.0
version: 3.0.0
ember-cli:
- specifier: ~6.5.0
- version: 6.5.0(handlebars@4.7.8)(underscore@1.13.7)
+ specifier: ~6.6.0
+ version: 6.6.0(handlebars@4.7.8)(underscore@1.13.7)
ember-cli-inject-live-reload:
specifier: ^2.1.0
version: 2.1.0
@@ -917,13 +917,13 @@ importers:
version: 1.1.3
ember-load-initializers:
specifier: ^3.0.1
- version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
ember-resolver:
specifier: ^13.1.1
version: 13.1.1
ember-source:
specifier: ~5.12.0
- version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-source-channel-url:
specifier: ^3.0.0
version: 3.0.0(encoding@0.1.13)
@@ -932,7 +932,7 @@ importers:
version: 4.7.0
webpack:
specifier: ^5.101.0
- version: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ version: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
app/assets/javascripts/select-kit:
dependencies:
@@ -953,10 +953,10 @@ importers:
version: link:../discourse-i18n
ember-auto-import:
specifier: ^2.10.0
- version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-cached-decorator-polyfill:
specifier: ^1.0.2
- version: 1.0.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 1.0.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
ember-cli-babel:
specifier: ^8.2.0
version: 8.2.0(@babel/core@7.28.0)
@@ -978,7 +978,7 @@ importers:
version: 2.2.0
'@embroider/test-setup':
specifier: ^4.0.0
- version: 4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
'@glimmer/component':
specifier: ^1.1.2
version: 1.1.2(@babel/core@7.28.0)
@@ -995,8 +995,8 @@ importers:
specifier: ^3.0.0
version: 3.0.0
ember-cli:
- specifier: ~6.5.0
- version: 6.5.0(handlebars@4.7.8)(underscore@1.13.7)
+ specifier: ~6.6.0
+ version: 6.6.0(handlebars@4.7.8)(underscore@1.13.7)
ember-cli-inject-live-reload:
specifier: ^2.1.0
version: 2.1.0
@@ -1011,13 +1011,13 @@ importers:
version: 1.1.3
ember-load-initializers:
specifier: ^3.0.1
- version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
ember-resolver:
specifier: ^13.1.1
version: 13.1.1
ember-source:
specifier: ~5.12.0
- version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-source-channel-url:
specifier: ^3.0.0
version: 3.0.0(encoding@0.1.13)
@@ -1026,7 +1026,7 @@ importers:
version: 4.7.0
webpack:
specifier: ^5.101.0
- version: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ version: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
app/assets/javascripts/theme-transpiler:
dependencies:
@@ -1043,8 +1043,8 @@ importers:
specifier: ^2.0.9
version: 2.0.9(postcss@8.5.6)
'@rollup/browser':
- specifier: ^4.46.1
- version: 4.46.1
+ specifier: ^4.46.2
+ version: 4.46.2
'@rollup/plugin-babel':
specifier: ^6.0.4
version: 6.0.4(@babel/core@7.28.0)(rollup@4.44.0)
@@ -1077,10 +1077,10 @@ importers:
version: 6.3.0
ember-source:
specifier: ~5.12.0
- version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-this-fallback:
specifier: ^0.4.0
- version: 0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))
+ version: 0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))
fastestsmallesttextencoderdecoder:
specifier: ^1.0.22
version: 1.0.22
@@ -1126,7 +1126,7 @@ importers:
version: 1.10.0
ember-auto-import:
specifier: ^2.10.0
- version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ version: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
packages:
@@ -1644,8 +1644,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-typescript@7.27.1':
- resolution: {integrity: sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==}
+ '@babel/plugin-transform-typescript@7.28.0':
+ resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -1697,8 +1697,8 @@ packages:
'@babel/runtime@7.12.18':
resolution: {integrity: sha512-BogPQ7ciE6SYAUPtlm9tWbgI9+2AgqSam6QivMgXgAT+fKbgppaj4ZX15MHeLC1PVF5sNk70huBu20XxWOs8Cg==}
- '@babel/runtime@7.27.6':
- resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
+ '@babel/runtime@7.28.2':
+ resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==}
engines: {node: '>=6.9.0'}
'@babel/standalone@7.28.2':
@@ -2365,8 +2365,8 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
- '@inquirer/figures@1.0.12':
- resolution: {integrity: sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==}
+ '@inquirer/figures@1.0.13':
+ resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==}
engines: {node: '>=18'}
'@isaacs/balanced-match@4.0.1':
@@ -2528,16 +2528,16 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
- '@pnpm/constants@1001.1.0':
- resolution: {integrity: sha512-xb9dfSGi1qfUKY3r4Zy9JdC9+ZeaDxwfE7HrrGIEsBVY1hvIn6ntbR7A97z3nk44yX7vwbINNf9sizTp0WEtEw==}
+ '@pnpm/constants@1001.2.0':
+ resolution: {integrity: sha512-ohlStawRU3+C1AvUPtfkK+HfZ/5UbDGO79dujS2dbTpzl3O03L3Ggk7nnH361ubbMYZqp4yWlX030xhMGpr8Nw==}
engines: {node: '>=18.12'}
- '@pnpm/error@1000.0.2':
- resolution: {integrity: sha512-2SfE4FFL73rE1WVIoESbqlj4sLy5nWW4M/RVdHvCRJPjlQHa9MH7m7CVJM204lz6I+eHoB+E7rL3zmpJR5wYnQ==}
+ '@pnpm/error@1000.0.3':
+ resolution: {integrity: sha512-Sr1wmJitZ768J+9MJVXXyd7tRaX3qThEiFH1K0AQovy2sIbLqo73MITphfVZH3YWIElt+Y1uMDnUZa676ZDA8g==}
engines: {node: '>=18.12'}
- '@pnpm/find-workspace-dir@1000.1.0':
- resolution: {integrity: sha512-K5iG/z0SLV6bVW1jIYvbNBI6vWAD6ETJKyWj/wwHr7hxloxtm9xJCGbe/41pmM9nfFFUPbr1Z0YOi4q9yWkj6g==}
+ '@pnpm/find-workspace-dir@1000.1.1':
+ resolution: {integrity: sha512-mI3FqyEcEXHO2c0YvGfvssaK3ITzKmc10DKRO+HdrN9zFoJxNMerzmVGqea+jeSlApDFzKqyrGCewITPZN9d2g==}
engines: {node: '>=18.12'}
'@popperjs/core@2.11.8':
@@ -2554,8 +2554,8 @@ packages:
peerDependencies:
prettier: 3.x
- '@rollup/browser@4.46.1':
- resolution: {integrity: sha512-GUgkcPlSkdYfwd24DDzLdzzYlpeCz+pN/1ZKvii2TwPjzwmmQRq3+AB/IogxgHxYfhVoR5pahsSBNkn+27S9Nw==}
+ '@rollup/browser@4.46.2':
+ resolution: {integrity: sha512-kNWmlqU13IPfGkAtgvNIOrqxgfN9WDHftPQ12H/7NI/kaGUTvlpgmL2ACzNE7qVD6a7RUVLZ++sUjYprVLtJIA==}
'@rollup/plugin-babel@6.0.4':
resolution: {integrity: sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==}
@@ -2728,68 +2728,68 @@ packages:
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
- '@swc/core-darwin-arm64@1.13.2':
- resolution: {integrity: sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==}
+ '@swc/core-darwin-arm64@1.13.3':
+ resolution: {integrity: sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [darwin]
- '@swc/core-darwin-x64@1.13.2':
- resolution: {integrity: sha512-Lb9EZi7X2XDAVmuUlBm2UvVAgSCbD3qKqDCxSI4jEOddzVOpNCnyZ/xEampdngUIyDDhhJLYU9duC+Mcsv5Y+A==}
+ '@swc/core-darwin-x64@1.13.3':
+ resolution: {integrity: sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==}
engines: {node: '>=10'}
cpu: [x64]
os: [darwin]
- '@swc/core-linux-arm-gnueabihf@1.13.2':
- resolution: {integrity: sha512-9TDe/92ee1x57x+0OqL1huG4BeljVx0nWW4QOOxp8CCK67Rpc/HHl2wciJ0Kl9Dxf2NvpNtkPvqj9+BUmM9WVA==}
+ '@swc/core-linux-arm-gnueabihf@1.13.3':
+ resolution: {integrity: sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==}
engines: {node: '>=10'}
cpu: [arm]
os: [linux]
- '@swc/core-linux-arm64-gnu@1.13.2':
- resolution: {integrity: sha512-KJUSl56DBk7AWMAIEcU83zl5mg3vlQYhLELhjwRFkGFMvghQvdqQ3zFOYa4TexKA7noBZa3C8fb24rI5sw9Exg==}
+ '@swc/core-linux-arm64-gnu@1.13.3':
+ resolution: {integrity: sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
- '@swc/core-linux-arm64-musl@1.13.2':
- resolution: {integrity: sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ==}
+ '@swc/core-linux-arm64-musl@1.13.3':
+ resolution: {integrity: sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
- '@swc/core-linux-x64-gnu@1.13.2':
- resolution: {integrity: sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ==}
+ '@swc/core-linux-x64-gnu@1.13.3':
+ resolution: {integrity: sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
- '@swc/core-linux-x64-musl@1.13.2':
- resolution: {integrity: sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw==}
+ '@swc/core-linux-x64-musl@1.13.3':
+ resolution: {integrity: sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
- '@swc/core-win32-arm64-msvc@1.13.2':
- resolution: {integrity: sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A==}
+ '@swc/core-win32-arm64-msvc@1.13.3':
+ resolution: {integrity: sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [win32]
- '@swc/core-win32-ia32-msvc@1.13.2':
- resolution: {integrity: sha512-PuCdtNynEkUNbUXX/wsyUC+t4mamIU5y00lT5vJcAvco3/r16Iaxl5UCzhXYaWZSNVZMzPp9qN8NlSL8M5pPxw==}
+ '@swc/core-win32-ia32-msvc@1.13.3':
+ resolution: {integrity: sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==}
engines: {node: '>=10'}
cpu: [ia32]
os: [win32]
- '@swc/core-win32-x64-msvc@1.13.2':
- resolution: {integrity: sha512-qlmMkFZJus8cYuBURx1a3YAG2G7IW44i+FEYV5/32ylKkzGNAr9tDJSA53XNnNXkAB5EXSPsOz7bn5C3JlEtdQ==}
+ '@swc/core-win32-x64-msvc@1.13.3':
+ resolution: {integrity: sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==}
engines: {node: '>=10'}
cpu: [x64]
os: [win32]
- '@swc/core@1.13.2':
- resolution: {integrity: sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg==}
+ '@swc/core@1.13.3':
+ resolution: {integrity: sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==}
engines: {node: '>=10'}
peerDependencies:
'@swc/helpers': '>=0.5.17'
@@ -2865,8 +2865,9 @@ packages:
'@types/fs-extra@8.1.5':
resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==}
- '@types/glob@8.1.0':
- resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==}
+ '@types/glob@9.0.0':
+ resolution: {integrity: sha512-00UxlRaIUvYm4R4W9WYkN8/J+kV8fmOQ7okeH6YFtGWFMt3odD45tpG5yA5wnL7HE6lLgjaTW5n14ju2hl2NNA==}
+ deprecated: This is a stub types definition. glob provides its own type definitions, so you do not need this installed.
'@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
@@ -2895,9 +2896,6 @@ packages:
'@types/minimatch@3.0.5':
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
- '@types/minimatch@5.1.2':
- resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
-
'@types/node@24.1.0':
resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==}
@@ -3853,8 +3851,8 @@ packages:
peerDependencies:
devtools-protocol: '*'
- ci-info@4.2.0:
- resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==}
+ ci-info@4.3.0:
+ resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==}
engines: {node: '>=8'}
class-utils@0.3.6:
@@ -3997,8 +3995,8 @@ packages:
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
engines: {node: '>= 0.6'}
- compression@1.8.0:
- resolution: {integrity: sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==}
+ compression@1.8.1:
+ resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
engines: {node: '>= 0.8.0'}
computeds@0.0.1:
@@ -4638,9 +4636,9 @@ packages:
resolution: {integrity: sha512-rk7GY+FmLn/2e22HsZs0Ycrz8HQ1W3Fv+2TFOuEFW9optnDXDgkntPBIl6gact/LHsfBM5RKbM3dHsIIeLgl0Q==}
engines: {node: 10.* || >= 12.*}
- ember-cli@6.5.0:
- resolution: {integrity: sha512-2qNqaD3iIFeFcYiKsgrsP0qdEilvT820+vX2Fz1x32XIgcsmy79ufc0rHrsHmEiazSQLC9XKUskwEzFBWjy54g==}
- engines: {node: '>= 18'}
+ ember-cli@6.6.0:
+ resolution: {integrity: sha512-YwiOuzB/qlTGsiSjsfPATi9YUm3j4SqOK7h9POw9tgLeFu34g7UPVver5MUIRQV8eQGVJ3TbgqRg8iXyNClgZQ==}
+ engines: {node: '>= 20.11'}
hasBin: true
ember-compatibility-helpers@1.2.7:
@@ -6810,8 +6808,8 @@ packages:
moo@0.5.2:
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
- morgan@1.10.0:
- resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==}
+ morgan@1.10.1:
+ resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==}
engines: {node: '>= 0.8.0'}
morphlex@0.0.16:
@@ -7062,8 +7060,8 @@ packages:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
- on-headers@1.0.2:
- resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
+ on-headers@1.1.0:
+ resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
engines: {node: '>= 0.8'}
once@1.4.0:
@@ -8864,8 +8862,8 @@ packages:
resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- validate-npm-package-name@6.0.1:
- resolution: {integrity: sha512-OaI//3H0J7ZkR1OqlhGA8cA+Cbk/2xFOQpJOt5+s27/ta9eZwpeervh4Mxh4w0im/kdgktowaqVNR7QOrUd7Yg==}
+ validate-npm-package-name@6.0.2:
+ resolution: {integrity: sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==}
engines: {node: ^18.17.0 || >=20.5.0}
vary@1.1.2:
@@ -9141,8 +9139,8 @@ packages:
workerpool@6.5.1:
resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==}
- workerpool@9.3.2:
- resolution: {integrity: sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==}
+ workerpool@9.3.3:
+ resolution: {integrity: sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
@@ -9884,7 +9882,7 @@ snapshots:
'@babel/core': 7.28.0(supports-color@8.1.1)
'@babel/helper-plugin-utils': 7.27.1
- '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)':
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
'@babel/helper-annotate-as-pure': 7.27.3
@@ -10019,7 +10017,7 @@ snapshots:
dependencies:
regenerator-runtime: 0.13.11
- '@babel/runtime@7.27.6': {}
+ '@babel/runtime@7.28.2': {}
'@babel/standalone@7.28.2': {}
@@ -10175,13 +10173,13 @@ snapshots:
'@ember/edition-utils@1.2.0': {}
- '@ember/legacy-built-in-components@0.5.0(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))':
+ '@ember/legacy-built-in-components@0.5.0(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))':
dependencies:
'@embroider/macros': 1.16.12(@glint/template@1.4.1-unstable.34c4510)
ember-cli-babel: 7.26.11
ember-cli-htmlbars: 5.7.2
ember-cli-typescript: 4.2.1
- ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
transitivePeerDependencies:
- '@glint/template'
- supports-color
@@ -10197,13 +10195,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@ember/render-modifiers@3.0.0(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))':
+ '@ember/render-modifiers@3.0.0(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))':
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
'@embroider/macros': 1.16.12(@glint/template@1.4.1-unstable.34c4510)
ember-cli-babel: 8.2.0(@babel/core@7.28.0)
ember-modifier-manager-polyfill: 1.2.0(@babel/core@7.28.0)
- ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
optionalDependencies:
'@glint/template': 1.4.1-unstable.34c4510
transitivePeerDependencies:
@@ -10250,11 +10248,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@embroider/babel-loader-9@3.1.1(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(supports-color@8.1.1)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))':
+ '@embroider/babel-loader-9@3.1.1(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(supports-color@8.1.1)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))':
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
'@embroider/core': 3.5.5(@glint/template@1.4.1-unstable.34c4510)
- babel-loader: 9.2.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ babel-loader: 9.2.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
transitivePeerDependencies:
- supports-color
- webpack
@@ -10268,7 +10266,7 @@ snapshots:
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0)
'@babel/plugin-transform-runtime': 7.26.10(@babel/core@7.28.0)
'@babel/preset-env': 7.28.0(@babel/core@7.28.0)(supports-color@8.1.1)
- '@babel/runtime': 7.27.6
+ '@babel/runtime': 7.28.2
'@babel/traverse': 7.28.0(supports-color@8.1.1)
'@embroider/core': 3.5.5(@glint/template@1.4.1-unstable.34c4510)
'@embroider/macros': 1.16.12(@glint/template@1.4.1-unstable.34c4510)
@@ -10346,10 +10344,10 @@ snapshots:
- supports-color
- utf-8-validate
- '@embroider/hbs-loader@3.0.3(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))':
+ '@embroider/hbs-loader@3.0.3(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))':
dependencies:
'@embroider/core': 3.5.5(@glint/template@1.4.1-unstable.34c4510)
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
'@embroider/macros@1.16.12(@glint/template@1.4.1-unstable.34c4510)':
dependencies:
@@ -10410,41 +10408,41 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@embroider/test-setup@4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))':
+ '@embroider/test-setup@4.0.0(@embroider/compat@3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510))(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))':
dependencies:
lodash: 4.17.21
resolve: 1.22.10
optionalDependencies:
'@embroider/compat': 3.8.5(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510)
'@embroider/core': 3.5.5(@glint/template@1.4.1-unstable.34c4510)
- '@embroider/webpack': 4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ '@embroider/webpack': 4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
- '@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))':
+ '@embroider/webpack@4.1.0(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))':
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
'@babel/preset-env': 7.28.0(@babel/core@7.28.0)(supports-color@8.1.1)
- '@embroider/babel-loader-9': 3.1.1(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(supports-color@8.1.1)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ '@embroider/babel-loader-9': 3.1.1(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(supports-color@8.1.1)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
'@embroider/core': 3.5.5(@glint/template@1.4.1-unstable.34c4510)
- '@embroider/hbs-loader': 3.0.3(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ '@embroider/hbs-loader': 3.0.3(@embroider/core@3.5.5(@glint/template@1.4.1-unstable.34c4510))(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
'@embroider/shared-internals': 2.9.0(supports-color@8.1.1)
'@types/supports-color': 8.1.3
assert-never: 1.4.0
- babel-loader: 8.4.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
- css-loader: 5.2.7(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ babel-loader: 8.4.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
+ css-loader: 5.2.7(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
csso: 4.2.0
debug: 4.4.1(supports-color@8.1.1)
escape-string-regexp: 4.0.0
fs-extra: 9.1.0
jsdom: 25.0.1(supports-color@8.1.1)
lodash: 4.17.21
- mini-css-extract-plugin: 2.9.2(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ mini-css-extract-plugin: 2.9.2(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
semver: 7.7.2
source-map-url: 0.4.1
- style-loader: 2.0.0(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ style-loader: 2.0.0(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
supports-color: 8.1.1
terser: 5.43.1
- thread-loader: 3.0.4(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ thread-loader: 3.0.4(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
transitivePeerDependencies:
- bufferutil
- canvas
@@ -10892,7 +10890,7 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
- '@inquirer/figures@1.0.12': {}
+ '@inquirer/figures@1.0.13': {}
'@isaacs/balanced-match@4.0.1': {}
@@ -11138,15 +11136,15 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
- '@pnpm/constants@1001.1.0': {}
+ '@pnpm/constants@1001.2.0': {}
- '@pnpm/error@1000.0.2':
+ '@pnpm/error@1000.0.3':
dependencies:
- '@pnpm/constants': 1001.1.0
+ '@pnpm/constants': 1001.2.0
- '@pnpm/find-workspace-dir@1000.1.0':
+ '@pnpm/find-workspace-dir@1000.1.1':
dependencies:
- '@pnpm/error': 1000.0.2
+ '@pnpm/error': 1000.0.3
find-up: 5.0.0
'@popperjs/core@2.11.8': {}
@@ -11174,7 +11172,7 @@ snapshots:
prettier: 3.5.3
rxjs: 6.6.7
- '@rollup/browser@4.46.1':
+ '@rollup/browser@4.46.2':
dependencies:
'@types/estree': 1.0.8
@@ -11314,51 +11312,51 @@ snapshots:
'@socket.io/component-emitter@3.1.2': {}
- '@swc/core-darwin-arm64@1.13.2':
+ '@swc/core-darwin-arm64@1.13.3':
optional: true
- '@swc/core-darwin-x64@1.13.2':
+ '@swc/core-darwin-x64@1.13.3':
optional: true
- '@swc/core-linux-arm-gnueabihf@1.13.2':
+ '@swc/core-linux-arm-gnueabihf@1.13.3':
optional: true
- '@swc/core-linux-arm64-gnu@1.13.2':
+ '@swc/core-linux-arm64-gnu@1.13.3':
optional: true
- '@swc/core-linux-arm64-musl@1.13.2':
+ '@swc/core-linux-arm64-musl@1.13.3':
optional: true
- '@swc/core-linux-x64-gnu@1.13.2':
+ '@swc/core-linux-x64-gnu@1.13.3':
optional: true
- '@swc/core-linux-x64-musl@1.13.2':
+ '@swc/core-linux-x64-musl@1.13.3':
optional: true
- '@swc/core-win32-arm64-msvc@1.13.2':
+ '@swc/core-win32-arm64-msvc@1.13.3':
optional: true
- '@swc/core-win32-ia32-msvc@1.13.2':
+ '@swc/core-win32-ia32-msvc@1.13.3':
optional: true
- '@swc/core-win32-x64-msvc@1.13.2':
+ '@swc/core-win32-x64-msvc@1.13.3':
optional: true
- '@swc/core@1.13.2':
+ '@swc/core@1.13.3':
dependencies:
'@swc/counter': 0.1.3
'@swc/types': 0.1.23
optionalDependencies:
- '@swc/core-darwin-arm64': 1.13.2
- '@swc/core-darwin-x64': 1.13.2
- '@swc/core-linux-arm-gnueabihf': 1.13.2
- '@swc/core-linux-arm64-gnu': 1.13.2
- '@swc/core-linux-arm64-musl': 1.13.2
- '@swc/core-linux-x64-gnu': 1.13.2
- '@swc/core-linux-x64-musl': 1.13.2
- '@swc/core-win32-arm64-msvc': 1.13.2
- '@swc/core-win32-ia32-msvc': 1.13.2
- '@swc/core-win32-x64-msvc': 1.13.2
+ '@swc/core-darwin-arm64': 1.13.3
+ '@swc/core-darwin-x64': 1.13.3
+ '@swc/core-linux-arm-gnueabihf': 1.13.3
+ '@swc/core-linux-arm64-gnu': 1.13.3
+ '@swc/core-linux-arm64-musl': 1.13.3
+ '@swc/core-linux-x64-gnu': 1.13.3
+ '@swc/core-linux-x64-musl': 1.13.3
+ '@swc/core-win32-arm64-msvc': 1.13.3
+ '@swc/core-win32-ia32-msvc': 1.13.3
+ '@swc/core-win32-x64-msvc': 1.13.3
'@swc/counter@0.1.3': {}
@@ -11443,10 +11441,9 @@ snapshots:
dependencies:
'@types/node': 24.1.0
- '@types/glob@8.1.0':
+ '@types/glob@9.0.0':
dependencies:
- '@types/minimatch': 5.1.2
- '@types/node': 24.1.0
+ glob: 11.0.3
'@types/http-errors@2.0.5': {}
@@ -11471,8 +11468,6 @@ snapshots:
'@types/minimatch@3.0.5': {}
- '@types/minimatch@5.1.2': {}
-
'@types/node@24.1.0':
dependencies:
undici-types: 7.8.0
@@ -11487,7 +11482,7 @@ snapshots:
'@types/rimraf@2.0.5':
dependencies:
- '@types/glob': 8.1.0
+ '@types/glob': 9.0.0
'@types/node': 24.1.0
'@types/rsvp@4.0.9': {}
@@ -12026,21 +12021,21 @@ snapshots:
babel-import-util@3.0.1: {}
- babel-loader@8.4.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ babel-loader@8.4.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
find-cache-dir: 3.3.2
loader-utils: 2.0.4
make-dir: 3.1.0
schema-utils: 2.7.1
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
- babel-loader@9.2.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ babel-loader@9.2.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
find-cache-dir: 4.0.0
schema-utils: 4.3.2
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
babel-plugin-debug-macros@0.2.0(@babel/core@7.28.0):
dependencies:
@@ -12133,7 +12128,7 @@ snapshots:
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
'@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.28.0)
+ '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
prettier: 2.8.8
transitivePeerDependencies:
- supports-color
@@ -12836,7 +12831,7 @@ snapshots:
mitt: 3.0.1
zod: 3.25.76
- ci-info@4.2.0: {}
+ ci-info@4.3.0: {}
class-utils@0.3.6:
dependencies:
@@ -12955,13 +12950,13 @@ snapshots:
dependencies:
mime-db: 1.54.0
- compression@1.8.0:
+ compression@1.8.1:
dependencies:
bytes: 3.1.2
compressible: 2.0.18
debug: 2.6.9
negotiator: 0.6.4
- on-headers: 1.0.2
+ on-headers: 1.1.0
safe-buffer: 5.2.1
vary: 1.1.2
transitivePeerDependencies:
@@ -13100,7 +13095,7 @@ snapshots:
css-functions-list@3.2.3: {}
- css-loader@5.2.7(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ css-loader@5.2.7(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
icss-utils: 5.1.0(postcss@8.5.6)
loader-utils: 2.0.4
@@ -13112,7 +13107,7 @@ snapshots:
postcss-value-parser: 4.2.0
schema-utils: 3.3.0
semver: 7.7.2
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
css-tree@1.1.3:
dependencies:
@@ -13317,7 +13312,7 @@ snapshots:
- '@glint/template'
- supports-color
- ember-auto-import@2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ ember-auto-import@2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
'@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.28.0)
@@ -13327,7 +13322,7 @@ snapshots:
'@babel/preset-env': 7.28.0(@babel/core@7.28.0)(supports-color@8.1.1)
'@embroider/macros': 1.16.12(@glint/template@1.4.1-unstable.34c4510)
'@embroider/shared-internals': 2.9.0(supports-color@8.1.1)
- babel-loader: 8.4.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ babel-loader: 8.4.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
babel-plugin-ember-modules-api-polyfill: 3.5.0
babel-plugin-ember-template-compilation: 2.4.1
babel-plugin-htmlbars-inline-precompile: 5.3.1
@@ -13337,7 +13332,7 @@ snapshots:
broccoli-merge-trees: 4.2.0
broccoli-plugin: 4.0.7
broccoli-source: 3.0.1
- css-loader: 5.2.7(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ css-loader: 5.2.7(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
debug: 4.4.1(supports-color@8.1.1)
fs-extra: 10.1.0
fs-tree-diff: 2.0.1
@@ -13345,14 +13340,14 @@ snapshots:
is-subdir: 1.2.0
js-string-escape: 1.0.1
lodash: 4.17.21
- mini-css-extract-plugin: 2.9.2(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ mini-css-extract-plugin: 2.9.2(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
minimatch: 3.1.2
parse5: 6.0.1
pkg-entry-points: 1.1.1
resolve: 1.22.10
resolve-package-path: 4.0.3
semver: 7.7.2
- style-loader: 2.0.0(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ style-loader: 2.0.0(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
typescript-memoize: 1.1.1
walk-sync: 3.0.0
transitivePeerDependencies:
@@ -13378,7 +13373,7 @@ snapshots:
- '@babel/core'
- supports-color
- ember-cached-decorator-polyfill@1.0.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))):
+ ember-cached-decorator-polyfill@1.0.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))):
dependencies:
'@embroider/macros': 1.16.12(@glint/template@1.4.1-unstable.34c4510)
'@glimmer/tracking': 1.1.2
@@ -13386,16 +13381,16 @@ snapshots:
ember-cache-primitive-polyfill: 1.0.1(@babel/core@7.28.0)
ember-cli-babel: 7.26.11
ember-cli-babel-plugin-helpers: 1.1.1
- ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
transitivePeerDependencies:
- '@babel/core'
- '@glint/template'
- supports-color
- ember-cli-app-version@7.0.0(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))):
+ ember-cli-app-version@7.0.0(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))):
dependencies:
ember-cli-babel: 7.26.11
- ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
git-repo-info: 2.1.1
transitivePeerDependencies:
- supports-color
@@ -13412,7 +13407,7 @@ snapshots:
'@babel/plugin-proposal-private-property-in-object': 7.21.11(@babel/core@7.28.0)
'@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.0)(supports-color@8.1.1)
'@babel/plugin-transform-runtime': 7.26.10(@babel/core@7.28.0)
- '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.28.0)
+ '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
'@babel/polyfill': 7.12.1
'@babel/preset-env': 7.28.0(@babel/core@7.28.0)(supports-color@8.1.1)
'@babel/runtime': 7.12.18
@@ -13448,7 +13443,7 @@ snapshots:
'@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.28.0)(supports-color@8.1.1)
'@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.0)(supports-color@8.1.1)
'@babel/plugin-transform-runtime': 7.26.10(@babel/core@7.28.0)
- '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.28.0)
+ '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
'@babel/preset-env': 7.28.0(@babel/core@7.28.0)(supports-color@8.1.1)
'@babel/runtime': 7.12.18
amd-name-resolver: 1.3.1
@@ -13470,11 +13465,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- ember-cli-deprecation-workflow@3.4.0(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))):
+ ember-cli-deprecation-workflow@3.4.0(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))):
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
ember-cli-babel: 8.2.0(@babel/core@7.28.0)
- ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
transitivePeerDependencies:
- supports-color
@@ -13636,9 +13631,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- ember-cli@6.5.0(handlebars@4.7.8)(underscore@1.13.7):
+ ember-cli@6.6.0(handlebars@4.7.8)(underscore@1.13.7):
dependencies:
- '@pnpm/find-workspace-dir': 1000.1.0
+ '@pnpm/find-workspace-dir': 1000.1.1
babel-remove-types: 1.0.1
broccoli: 3.5.2
broccoli-concat: 4.2.5
@@ -13655,9 +13650,9 @@ snapshots:
calculate-cache-key-for-tree: 2.0.0
capture-exit: 2.0.0
chalk: 4.1.2
- ci-info: 4.2.0
+ ci-info: 4.3.0
clean-base-url: 1.0.0
- compression: 1.8.0
+ compression: 1.8.1
configstore: 5.0.1
console-ui: 3.1.2
content-tag: 3.1.3
@@ -13695,7 +13690,7 @@ snapshots:
markdown-it: 14.1.0
markdown-it-terminal: 0.4.0(markdown-it@14.1.0)
minimatch: 7.4.6
- morgan: 1.10.0
+ morgan: 1.10.1
nopt: 3.0.6
npm-package-arg: 12.0.2
os-locale: 5.0.0
@@ -13718,7 +13713,7 @@ snapshots:
tree-sync: 2.1.0
walk-sync: 3.0.0
watch-detector: 1.0.2
- workerpool: 9.3.2
+ workerpool: 9.3.3
yam: 1.0.0
transitivePeerDependencies:
- arc-templates
@@ -13819,16 +13814,16 @@ snapshots:
transitivePeerDependencies:
- eslint
- ember-exam@9.1.0(@glint/template@1.4.1-unstable.34c4510)(ember-qunit@9.0.3(@ember/test-helpers@5.2.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510)(qunit@2.24.1))(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)))(qunit@2.24.1)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ ember-exam@9.1.0(@glint/template@1.4.1-unstable.34c4510)(ember-qunit@9.0.3(@ember/test-helpers@5.2.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510)(qunit@2.24.1))(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)))(qunit@2.24.1)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
chalk: 5.4.1
cli-table3: 0.6.5
debug: 4.4.1(supports-color@8.1.1)
- ember-auto-import: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ ember-auto-import: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-cli-babel: 8.2.0(@babel/core@7.28.0)
ember-qunit: 9.0.3(@ember/test-helpers@5.2.2(@babel/core@7.28.0)(@glint/template@1.4.1-unstable.34c4510))(@glint/template@1.4.1-unstable.34c4510)(qunit@2.24.1)
- ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
execa: 8.0.1
fs-extra: 11.3.0
js-yaml: 4.1.0
@@ -13842,9 +13837,9 @@ snapshots:
- supports-color
- webpack
- ember-load-initializers@3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))):
+ ember-load-initializers@3.0.1(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))):
dependencies:
- ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-modifier-manager-polyfill@1.2.0(@babel/core@7.28.0):
dependencies:
@@ -13904,7 +13899,7 @@ snapshots:
transitivePeerDependencies:
- encoding
- ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
'@ember/edition-utils': 1.2.0
@@ -13932,7 +13927,7 @@ snapshots:
broccoli-funnel: 3.0.8
broccoli-merge-trees: 4.2.0
chalk: 4.1.2
- ember-auto-import: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ ember-auto-import: 2.10.0(@glint/template@1.4.1-unstable.34c4510)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
ember-cli-babel: 8.2.0(@babel/core@7.28.0)
ember-cli-get-component-path-option: 1.0.0
ember-cli-is-package-missing: 1.0.0
@@ -13976,7 +13971,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- ember-this-fallback@0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))):
+ ember-this-fallback@0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))):
dependencies:
'@glimmer/syntax': 0.84.3
babel-plugin-ember-template-compilation: 2.4.1
@@ -13984,7 +13979,7 @@ snapshots:
ember-cli-babel: 7.26.11
ember-cli-htmlbars: 6.3.0
ember-cli-typescript: 5.3.0
- ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ ember-source: 5.12.0(patch_hash=pqnuctuxbp6ekxyjszyhrgmysm)(@glimmer/component@1.1.2(@babel/core@7.28.0))(@glint/template@1.4.1-unstable.34c4510)(rsvp@4.8.5)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
lodash: 4.17.21
winston: 3.14.2
zod: 3.25.76
@@ -15413,11 +15408,11 @@ snapshots:
import-meta-resolve@4.1.0: {}
- imports-loader@5.0.0(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ imports-loader@5.0.0(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
source-map-js: 1.2.1
strip-comments: 2.0.1
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
imurmurhash@0.1.4: {}
@@ -15474,7 +15469,7 @@ snapshots:
inquirer@9.3.7:
dependencies:
- '@inquirer/figures': 1.0.12
+ '@inquirer/figures': 1.0.13
ansi-escapes: 4.3.2
cli-width: 4.1.0
external-editor: 3.1.0
@@ -15613,7 +15608,7 @@ snapshots:
is-language-code@3.1.0:
dependencies:
- '@babel/runtime': 7.27.6
+ '@babel/runtime': 7.28.2
is-map@2.0.3: {}
@@ -16347,11 +16342,11 @@ snapshots:
dependencies:
dom-walk: 0.1.2
- mini-css-extract-plugin@2.9.2(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ mini-css-extract-plugin@2.9.2(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
schema-utils: 4.3.2
tapable: 2.2.2
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
minimatch@10.0.3:
dependencies:
@@ -16448,13 +16443,13 @@ snapshots:
moo@0.5.2: {}
- morgan@1.10.0:
+ morgan@1.10.1:
dependencies:
basic-auth: 2.0.1
debug: 2.6.9
depd: 2.0.0
on-finished: 2.3.0
- on-headers: 1.0.2
+ on-headers: 1.1.0
transitivePeerDependencies:
- supports-color
@@ -16605,7 +16600,7 @@ snapshots:
hosted-git-info: 8.1.0
proc-log: 5.0.0
semver: 7.7.2
- validate-npm-package-name: 6.0.1
+ validate-npm-package-name: 6.0.2
npm-packlist@8.0.2:
dependencies:
@@ -16722,7 +16717,7 @@ snapshots:
dependencies:
ee-first: 1.1.1
- on-headers@1.0.2: {}
+ on-headers@1.1.0: {}
once@1.4.0:
dependencies:
@@ -17438,7 +17433,7 @@ snapshots:
dependencies:
'@babel/core': 7.28.0(supports-color@8.1.1)
'@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.28.0)
+ '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
prettier: 2.8.8
transitivePeerDependencies:
- supports-color
@@ -18200,11 +18195,11 @@ snapshots:
strip-test-selectors@0.1.0: {}
- style-loader@2.0.0(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ style-loader@2.0.0(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
loader-utils: 2.0.4
schema-utils: 3.3.0
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
styled_string@0.0.1: {}
@@ -18387,16 +18382,16 @@ snapshots:
temporal-spec@0.2.4: {}
- terser-webpack-plugin@5.3.14(@swc/core@1.13.2)(esbuild@0.25.8)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ terser-webpack-plugin@5.3.14(@swc/core@1.13.3)(esbuild@0.25.8)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
'@jridgewell/trace-mapping': 0.3.29
jest-worker: 27.5.1
schema-utils: 4.3.2
serialize-javascript: 6.0.2
terser: 5.43.1
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
optionalDependencies:
- '@swc/core': 1.13.2
+ '@swc/core': 1.13.3
esbuild: 0.25.8
terser@5.43.1:
@@ -18413,7 +18408,7 @@ snapshots:
bluebird: 3.7.2
charm: 1.0.2
commander: 2.20.3
- compression: 1.8.0
+ compression: 1.8.1
consolidate: 0.16.0(handlebars@4.7.8)(lodash@4.17.21)(mustache@4.2.0)(underscore@1.13.7)
execa: 1.0.0
express: 4.21.2
@@ -18502,14 +18497,14 @@ snapshots:
dependencies:
tslib: 2.8.1
- thread-loader@3.0.4(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ thread-loader@3.0.4(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
json-parse-better-errors: 1.0.2
loader-runner: 4.3.0
loader-utils: 2.0.4
neo-async: 2.6.2
schema-utils: 3.3.0
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
through2-filter@3.0.0:
dependencies:
@@ -18851,7 +18846,7 @@ snapshots:
validate-npm-package-name@5.0.1: {}
- validate-npm-package-name@6.0.1: {}
+ validate-npm-package-name@6.0.2: {}
vary@1.1.2: {}
@@ -19039,16 +19034,16 @@ snapshots:
webidl-conversions@7.0.0: {}
- webpack-retry-chunk-load-plugin@3.1.1(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)):
+ webpack-retry-chunk-load-plugin@3.1.1(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)):
dependencies:
prettier: 2.8.8
- webpack: 5.101.0(@swc/core@1.13.2)(esbuild@0.25.8)
+ webpack: 5.101.0(@swc/core@1.13.3)(esbuild@0.25.8)
webpack-sources@3.3.3: {}
webpack-stats-plugin@1.1.3: {}
- webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8):
+ webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.8
@@ -19072,7 +19067,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 4.3.2
tapable: 2.2.2
- terser-webpack-plugin: 5.3.14(@swc/core@1.13.2)(esbuild@0.25.8)(webpack@5.101.0(@swc/core@1.13.2)(esbuild@0.25.8))
+ terser-webpack-plugin: 5.3.14(@swc/core@1.13.3)(esbuild@0.25.8)(webpack@5.101.0(@swc/core@1.13.3)(esbuild@0.25.8))
watchpack: 2.4.4
webpack-sources: 3.3.3
transitivePeerDependencies:
@@ -19202,7 +19197,7 @@ snapshots:
workerpool@6.5.1: {}
- workerpool@9.3.2: {}
+ workerpool@9.3.3: {}
wrap-ansi@6.2.0:
dependencies:
diff --git a/spec/lib/validators/form_template_yaml_validator_spec.rb b/spec/lib/validators/form_template_yaml_validator_spec.rb
index af368eb884115..037685f43fe61 100644
--- a/spec/lib/validators/form_template_yaml_validator_spec.rb
+++ b/spec/lib/validators/form_template_yaml_validator_spec.rb
@@ -186,4 +186,42 @@
end
end
end
+
+ describe "#check_tag_groups" do
+ fab!(:tag_group)
+
+ context "when tag group names are valid" do
+ let(:yaml_content) { <<~YAML }
+ - type: tag-chooser
+ id: name
+ tag_group: "#{tag_group.name}"
+ YAML
+
+ it "does not add an error" do
+ validator.validate(form_template)
+ expect(form_template.errors[:template]).to be_empty
+ end
+ end
+
+ context "when tag group names contains invalid name" do
+ let(:yaml_content) { <<~YAML }
+ - type: tag-chooser
+ id: name1
+ tag_group: "#{tag_group.name}"
+ - type: tag-chooser
+ id: name2
+ tag_group: "invalid tag group name"
+ YAML
+
+ it "adds an error for invalid tag groups" do
+ validator.validate(form_template)
+ expect(form_template.errors[:template]).to include(
+ I18n.t(
+ "form_templates.errors.invalid_tag_group",
+ tag_group_name: "invalid tag group name",
+ ),
+ )
+ end
+ end
+ end
end
diff --git a/spec/models/form_template_spec.rb b/spec/models/form_template_spec.rb
index d27afd21c6233..b71434cd93fc3 100644
--- a/spec/models/form_template_spec.rb
+++ b/spec/models/form_template_spec.rb
@@ -116,7 +116,7 @@
g1, g2 = YAML.safe_load(form_template.template)
- expect(g1["choices"]).to eq([tag1.name, tag2.name, tag3.name])
+ expect(g1["choices"]).to eq([tag1.name, tag2.name, tag3.name].sort)
expect(g2["attributes"]["tag_group"]).to eq(tag_group2.name)
expect(g2["attributes"]["multiple"]).to eq(true)
end
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
index 6747125c38313..329a4c7b81c57 100644
--- a/spec/models/post_spec.rb
+++ b/spec/models/post_spec.rb
@@ -1655,6 +1655,42 @@ def post_with_body(body, user = nil)
1,
)
end
+
+ context "in a topic with multiple replies" do
+ let!(:second_last_reply) do
+ freeze_time 1.day.from_now
+ create_post(topic:, user: coding_horror, hidden: true)
+ end
+ fab!(:user)
+ let!(:last_reply) do
+ freeze_time 2.days.from_now
+ create_post(topic:, user:, hidden: true)
+ end
+ let!(:whisper_post) do
+ freeze_time 3.days.from_now
+ create_post(topic:, user: user, post_type: Post.types[:whisper], hidden: true)
+ end
+
+ before { topic.update_columns(bumped_at: 1.day.from_now) }
+
+ it "does not reset the topic's bumped_at when unhiding a whisper" do
+ whisper_post.unhide!
+
+ expect(topic.reload.bumped_at).to eq_time(1.day.from_now)
+ end
+
+ it "resets the topic's bumped_at when unhiding the last visible reply" do
+ last_reply.unhide!
+
+ expect(topic.reload.bumped_at).to eq_time(last_reply.created_at)
+ end
+
+ it "does not reset the topic's bumped_at when unhiding a reply that is not the last" do
+ second_last_reply.unhide!
+
+ expect(topic.reload.bumped_at).to eq_time(1.day.from_now)
+ end
+ end
end
it "will unhide the post but will keep the topic invisible/unlisted" do
diff --git a/spec/requests/admin/form_templates_controller_spec.rb b/spec/requests/admin/form_templates_controller_spec.rb
index ae982843ba239..382c12bd1cd42 100644
--- a/spec/requests/admin/form_templates_controller_spec.rb
+++ b/spec/requests/admin/form_templates_controller_spec.rb
@@ -204,7 +204,7 @@
expect(response.status).to eq(200)
processed_tag_group =
YAML.safe_load(response.parsed_body["form_template"]["template"]).first
- expect(processed_tag_group["choices"]).to eq([tag1.name, tag2.name, tag3.name])
+ expect(processed_tag_group["choices"]).to eq([tag1.name, tag2.name, tag3.name].sort)
end
it "rejects invalid templates" do
diff --git a/spec/requests/api/schemas/json/user_get_response.json b/spec/requests/api/schemas/json/user_get_response.json
index 6e2854d2cc4b5..c2c94d65fe9b9 100644
--- a/spec/requests/api/schemas/json/user_get_response.json
+++ b/spec/requests/api/schemas/json/user_get_response.json
@@ -815,6 +815,9 @@
},
"topics_unread_when_closed": {
"type": "boolean"
+ },
+ "composition_mode": {
+ "type": "integer"
}
},
"required": [
diff --git a/spec/system/composer/default_to_subcategory_spec.rb b/spec/system/composer/default_to_subcategory_spec.rb
index 272c80ad5fde9..83e289d3d2329 100644
--- a/spec/system/composer/default_to_subcategory_spec.rb
+++ b/spec/system/composer/default_to_subcategory_spec.rb
@@ -41,14 +41,9 @@
end
end
describe "Category does not have subcategory" do
- it "should have the 'New Topic' button enabled and default category set in the composer" do
+ it "should have the 'New Topic' button disabled" do
category_page.visit(category_with_no_subcategory)
- expect(category_page).to have_button("New Topic", disabled: false)
-
- category_page.new_topic_button.click
- select_kit =
- PageObjects::Components::SelectKit.new("#reply-control.open .category-chooser")
- expect(select_kit).to have_selected_value(default_latest_category.id)
+ expect(category_page).to have_button("New Topic", disabled: true)
end
end
end
@@ -71,14 +66,9 @@
end
describe "Can't post on parent category" do
describe "Category does not have subcategory" do
- it "opens composer without a preset topic" do
+ it "should have the 'New Topic' button disabled" do
category_page.visit(category_with_no_subcategory)
- expect(category_page).to have_button("New Topic", disabled: false)
- category_page.find("#create-topic").click
-
- select_kit =
- PageObjects::Components::SelectKit.new("#reply-control.open .category-chooser")
- expect(select_kit).to have_selected_name("category…")
+ expect(category_page).to have_button("New Topic", disabled: true)
end
end
end
@@ -97,18 +87,10 @@
end
describe "Setting disabled and can't post on parent category" do
- before do
- SiteSetting.default_subcategory_on_read_only_category = false
- SiteSetting.default_composer_category = default_latest_category.id
- end
-
- it "opens composer with default composer category selected" do
+ before { SiteSetting.default_subcategory_on_read_only_category = false }
+ it "should have 'New Topic' button disabled" do
category_page.visit(category)
- expect(category_page).to have_button("New Topic", disabled: false)
- page.find("#create-topic").click
-
- select_kit = PageObjects::Components::SelectKit.new("#reply-control.open .category-chooser")
- expect(select_kit).to have_selected_name(default_latest_category.name)
+ expect(category_page).to have_button("New Topic", disabled: true)
end
end
end
diff --git a/spec/system/composer/prosemirror_editor_spec.rb b/spec/system/composer/prosemirror_editor_spec.rb
index 18d7893de56f6..8caa50bd65f5b 100644
--- a/spec/system/composer/prosemirror_editor_spec.rb
+++ b/spec/system/composer/prosemirror_editor_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
describe "Composer - ProseMirror editor", type: :system do
- fab!(:user) do
+ fab!(:current_user) do
Fabricate(
:user,
refresh_auto_groups: true,
@@ -14,7 +14,7 @@
let(:composer) { PageObjects::Components::Composer.new }
let(:rich) { composer.rich_editor }
- before { sign_in(user) }
+ before { sign_in(current_user) }
def open_composer
page.visit "/new-topic"
@@ -40,16 +40,29 @@ def paste_and_click_image
expect(composer).to have_composer_preview_toggle
end
- it "saves the user's rich editor preference to the database" do
+ it "saves the user's rich editor preference and remembers it when reopening the composer" do
open_composer
+ expect(composer).to have_rich_editor_active
composer.toggle_rich_editor
- expect(page).to have_css(".composer-toggle-switch.--markdown")
+ expect(composer).to have_markdown_editor_active
try_until_success(frequency: 0.5) do
- expect(user.user_option.reload.composition_mode).to eq(
+ expect(current_user.user_option.reload.composition_mode).to eq(
UserOption.composition_mode_types[:markdown],
)
end
+
+ visit("/")
+ open_composer
+ expect(composer).to have_markdown_editor_active
+ end
+
+ it "remembers the user's rich editor preference when starting a new PM" do
+ current_user.user_option.update!(composition_mode: UserOption.composition_mode_types[:rich])
+ page.visit("/u/#{current_user.username}/messages")
+ find(".new-private-message").click
+ expect(composer).to be_opened
+ expect(composer).to have_rich_editor_active
end
# TODO (martin) Remove this once we are sure all users have migrated
@@ -68,7 +81,7 @@ def paste_and_click_image
expect(composer).to have_rich_editor
try_until_success(frequency: 0.5) do
- expect(user.user_option.reload.composition_mode).to eq(
+ expect(current_user.user_option.reload.composition_mode).to eq(
UserOption.composition_mode_types[:rich],
)
end
@@ -83,7 +96,7 @@ def paste_and_click_image
context "with autocomplete" do
it "triggers an autocomplete on mention" do
open_composer
- composer.type_content("@#{user.username}")
+ composer.type_content("@#{current_user.username}")
expect(composer).to have_mention_autocomplete
end
@@ -939,19 +952,19 @@ def body(title)
before do
Draft.set(
- user,
+ current_user,
topic.draft_key,
0,
- { reply: "hey @#{user.username} and @unknown - how are you?" }.to_json,
+ { reply: "hey @#{current_user.username} and @unknown - how are you?" }.to_json,
)
end
it "validates manually typed mentions" do
open_composer
- composer.type_content("Hey @#{user.username} ")
+ composer.type_content("Hey @#{current_user.username} ")
- expect(rich).to have_css("a.mention", text: user.username)
+ expect(rich).to have_css("a.mention", text: current_user.username)
composer.type_content("and @invalid_user - how are you?")
@@ -959,7 +972,9 @@ def body(title)
composer.toggle_rich_editor
- expect(composer).to have_value("Hey @#{user.username} and @invalid_user - how are you?")
+ expect(composer).to have_value(
+ "Hey @#{current_user.username} and @invalid_user - how are you?",
+ )
end
it "validates mentions in drafts" do
@@ -967,7 +982,7 @@ def body(title)
expect(composer).to be_opened
- expect(rich).to have_css("a.mention", text: user.username)
+ expect(rich).to have_css("a.mention", text: current_user.username)
expect(rich).to have_no_css("a.mention", text: "@unknown")
end
@@ -1055,7 +1070,7 @@ def body(title)
upsert_hyperlink_modal.fill_in_link_text("Updated Example")
upsert_hyperlink_modal.fill_in_link_url("https://updated-example.com")
- upsert_hyperlink_modal.click_primary_button
+ upsert_hyperlink_modal.send_enter_link_text
expect(rich).to have_css("a[href='https://updated-example.com']", text: "Updated Example")
diff --git a/spec/system/composer_spec.rb b/spec/system/composer_spec.rb
index fbeffd16ea1a3..5c417f99428dd 100644
--- a/spec/system/composer_spec.rb
+++ b/spec/system/composer_spec.rb
@@ -5,6 +5,7 @@
let(:composer) { PageObjects::Components::Composer.new }
before { sign_in(user) }
+ before { SiteSetting.floatkit_autocomplete_composer = false }
it "displays user cards in preview" do
page.visit "/new-topic"
@@ -73,4 +74,76 @@
)
end
end
+
+ context "with floatkit autocomplete enabled" do
+ before { SiteSetting.floatkit_autocomplete_composer = true }
+
+ it "displays user cards in preview" do
+ page.visit "/new-topic"
+
+ expect(composer).to be_opened
+
+ composer.fill_content("@#{user.username}")
+ composer.preview.find("a.mention").click
+
+ page.has_css?("#user-card")
+ end
+
+ context "in a topic, the autocomplete prioritizes" do
+ fab!(:topic_user, :user)
+ fab!(:second_reply_user, :user)
+
+ fab!(:topic) { Fabricate(:topic, user: topic_user) }
+ fab!(:op) { Fabricate(:post, topic: topic, user: topic_user) }
+ let!(:op_post) { PageObjects::Components::Post.new(op.post_number) }
+
+ fab!(:second_reply) { Fabricate(:post, topic: topic, user: second_reply_user) }
+ let!(:second_reply_post) { PageObjects::Components::Post.new(second_reply.post_number) }
+
+ before { SiteSetting.enable_names = false }
+
+ it "the topic owner if replying to topic" do
+ page.visit "/t/#{topic.id}"
+
+ op_post.reply
+ expect(composer).to be_opened
+ composer.type_content("@")
+
+ expect(composer.mention_menu_autocomplete_username_list).to eq(
+ [op.username, second_reply_user.username], # must be first the topic owner
+ )
+ end
+
+ it "the recipient of the reply when replying" do
+ page.visit "/t/#{topic.id}"
+
+ second_reply_post.reply
+ expect(composer).to be_opened
+ composer.type_content("@")
+
+ expect(composer.mention_menu_autocomplete_username_list).to eq(
+ [second_reply_user.username, topic_user.username], # must be first the reply user
+ )
+ end
+
+ it "the recipient of the reply when editing a reply" do
+ admin = Fabricate(:admin, refresh_auto_groups: true)
+ reply_to_second_post =
+ Fabricate(:post, topic: topic, user: user, reply_to_post_number: second_reply.post_number)
+ reply_post = PageObjects::Components::Post.new(reply_to_second_post.post_number)
+
+ sign_in(admin)
+ page.visit "/t/#{topic.id}"
+ reply_post.edit
+
+ expect(composer).to be_opened
+
+ composer.type_content(" @")
+
+ expect(composer.mention_menu_autocomplete_username_list).to eq(
+ [second_reply_user.username, user.username, topic_user.username],
+ )
+ end
+ end
+ end
end
diff --git a/spec/system/drafts_dropdown_spec.rb b/spec/system/drafts_dropdown_spec.rb
index 2583ec5883870..7cd37aeaf3d31 100644
--- a/spec/system/drafts_dropdown_spec.rb
+++ b/spec/system/drafts_dropdown_spec.rb
@@ -2,7 +2,6 @@
describe "Drafts dropdown", type: :system do
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
- fab!(:category)
let(:composer) { PageObjects::Components::Composer.new }
let(:drafts_dropdown) { PageObjects::Components::DraftsMenu.new }
let(:discard_draft_modal) { PageObjects::Modals::DiscardDraft.new }
@@ -111,10 +110,10 @@
)
end
- it "is still enabled" do
+ it "disables the drafts dropdown menu when new topic button is disabled" do
category_page.visit(category)
- expect(category_page).to have_button("New Topic", disabled: false)
+ expect(category_page).to have_button("New Topic", disabled: true)
expect(drafts_dropdown).to be_enabled
end
end
diff --git a/spec/system/edit_category_images_spec.rb b/spec/system/edit_category_images_spec.rb
index 9309a90244e06..5bacf53950fd9 100644
--- a/spec/system/edit_category_images_spec.rb
+++ b/spec/system/edit_category_images_spec.rb
@@ -26,7 +26,7 @@
)
expect(page).to have_content("uploaded successfully").or have_css(
- ".uploaded-image-preview.input-xxlarge",
+ ".has-image .uploaded-image-preview.input-xxlarge",
)
upload = Upload.last
diff --git a/spec/system/edit_category_localizations_spec.rb b/spec/system/edit_category_localizations_spec.rb
index e797f4568a77e..bb16e4dab9a4c 100644
--- a/spec/system/edit_category_localizations_spec.rb
+++ b/spec/system/edit_category_localizations_spec.rb
@@ -82,11 +82,16 @@
end
describe "when editing a category with localizations" do
- fab!(:category_localization) { Fabricate(:category_localization, category: category) }
+ fab!(:category_localization) { Fabricate(:category_localization, category:, locale: "es") }
it "allows you to delete localizations" do
expect(CategoryLocalization.where(category_id: category.id).count).to eq(1)
category_page.visit_edit_localizations(category)
+
+ expect(
+ category_page.find("#control-localizations-0-locale option.--selected"),
+ ).to have_content("Spanish (Español)")
+
page.find(".edit-category-tab-localizations .remove-localization").click
category_page.save_settings
page.refresh
diff --git a/spec/system/hashtag_autocomplete_spec.rb b/spec/system/hashtag_autocomplete_spec.rb
index fe4cd8aec153e..13a500d2b899c 100644
--- a/spec/system/hashtag_autocomplete_spec.rb
+++ b/spec/system/hashtag_autocomplete_spec.rb
@@ -17,6 +17,7 @@
let(:topic_page) { PageObjects::Pages::Topic.new }
before { sign_in(current_user) }
+ before { SiteSetting.floatkit_autocomplete_composer = false }
def visit_topic_and_initiate_autocomplete(initiation_text: "something #co", expected_count: 2)
topic_page.visit_topic_and_open_composer(topic)
@@ -261,4 +262,129 @@ def visit_topic_and_initiate_autocomplete(initiation_text: "something #co", expe
expect(generated_css).not_to include(".hashtag-color--category--#{private_category.id}")
end
end
+
+ context "with floatkit autocomplete enabled" do
+ before { SiteSetting.floatkit_autocomplete_composer = true }
+
+ it "searches for categories and tags with # and prioritises categories in the results" do
+ visit_topic_and_initiate_autocomplete
+ hashtag_results = page.all(".hashtag-autocomplete__link", count: 2)
+ expect(hashtag_results.map(&:text).map { |r| r.gsub("\n", " ") }).to eq(
+ ["Cool Category", "cooltag (x325)"],
+ )
+ end
+
+ it "begins showing results as soon as # is pressed based on categories and tags topic_count" do
+ visit_topic_and_initiate_autocomplete(initiation_text: "#", expected_count: 5)
+ hashtag_results = page.all(".hashtag-autocomplete__link")
+ expect(hashtag_results.map(&:text).map { |r| r.gsub("\n", " ") }).to eq(
+ [
+ "Cool Category",
+ "Other Category",
+ uncategorized_category.name,
+ "cooltag (x325)",
+ "othertag (x66)",
+ ],
+ )
+ end
+
+ it "cooks the selected hashtag clientside in the composer preview with the correct url and icon" do
+ visit_topic_and_initiate_autocomplete
+ hashtag_results = page.all(".hashtag-autocomplete__link", count: 2)
+ hashtag_results[0].click
+ expect(page).to have_css(".hashtag-cooked")
+ cooked_hashtag = page.find(".hashtag-cooked")
+
+ expect(cooked_hashtag["outerHTML"]).to have_tag(
+ "a",
+ with: {
+ class: "hashtag-cooked",
+ href: category.url,
+ "data-type": "category",
+ "data-slug": category.slug,
+ "data-id": category.id,
+ },
+ ) do
+ with_tag(
+ "span",
+ with: {
+ class: "hashtag-category-square hashtag-color--category-#{category.id}",
+ },
+ )
+ end
+
+ visit_topic_and_initiate_autocomplete
+ hashtag_results = page.all(".hashtag-autocomplete__link", count: 2)
+ hashtag_results[1].click
+ expect(page).to have_css(".hashtag-cooked")
+ cooked_hashtag = page.find(".hashtag-cooked")
+ expect(cooked_hashtag["outerHTML"]).to have_tag(
+ "a",
+ with: {
+ class: "hashtag-cooked",
+ href: tag.url,
+ "data-type": "tag",
+ "data-slug": tag.name,
+ "data-id": tag.id,
+ },
+ ) do
+ with_tag(
+ "svg",
+ with: {
+ class: "fa d-icon d-icon-tag svg-icon hashtag-color--tag-#{tag.id} svg-string",
+ },
+ ) { with_tag("use", with: { href: "#tag" }) }
+ end
+ end
+
+ it "cooks the hashtags for tag and category correctly serverside when the post is saved to the database" do
+ topic_page.visit_topic_and_open_composer(topic)
+
+ expect(topic_page).to have_expanded_composer
+
+ topic_page.send_reply("this is a #cool-cat category and a #cooltag tag")
+
+ expect(topic_page).to have_post_number(2)
+
+ cooked_hashtags = page.all(".hashtag-cooked", count: 2)
+
+ expect(cooked_hashtags[0]["outerHTML"]).to have_tag(
+ "a",
+ with: {
+ class: "hashtag-cooked",
+ href: category.url,
+ "data-type": "category",
+ "data-slug": category.slug,
+ "data-id": category.id,
+ "aria-label": category.name,
+ },
+ ) do
+ with_tag(
+ "span",
+ with: {
+ class: "hashtag-category-square hashtag-color--category-#{category.id}",
+ },
+ )
+ end
+
+ expect(cooked_hashtags[1]["outerHTML"]).to have_tag(
+ "a",
+ with: {
+ class: "hashtag-cooked",
+ href: tag.url,
+ "data-type": "tag",
+ "data-slug": tag.name,
+ "data-id": tag.id,
+ "aria-label": tag.name,
+ },
+ ) do
+ with_tag(
+ "svg",
+ with: {
+ class: "fa d-icon d-icon-tag svg-icon hashtag-color--tag-#{tag.id} svg-string",
+ },
+ ) { with_tag("use", with: { href: "#tag" }) }
+ end
+ end
+ end
end
diff --git a/spec/system/page_objects/components/composer.rb b/spec/system/page_objects/components/composer.rb
index bad738a68d43e..5018e8eb712ea 100644
--- a/spec/system/page_objects/components/composer.rb
+++ b/spec/system/page_objects/components/composer.rb
@@ -329,11 +329,15 @@ def select_pm_user(username)
end
def has_rich_editor_active?
- find("#{COMPOSER_ID}").has_css?(".composer-toggle-switch__right-icon.--active")
+ find("#{COMPOSER_ID}").has_css?(".composer-toggle-switch.--rte")
end
def has_no_rich_editor_active?
- find("#{COMPOSER_ID}").has_css?(".composer-toggle-switch__left-icon.--active")
+ find("#{COMPOSER_ID}").has_css?(".composer-toggle-switch.--markdown")
+ end
+
+ def has_markdown_editor_active?
+ has_no_rich_editor_active?
end
def toggle_rich_editor
diff --git a/spec/system/page_objects/modals/upsert_hyperlink.rb b/spec/system/page_objects/modals/upsert_hyperlink.rb
index 3991d23ceec94..28e641e5e0c0b 100644
--- a/spec/system/page_objects/modals/upsert_hyperlink.rb
+++ b/spec/system/page_objects/modals/upsert_hyperlink.rb
@@ -11,6 +11,10 @@ def fill_in_link_text(text)
find(LINK_TEXT_SELECTOR).fill_in(with: text)
end
+ def send_enter_link_text
+ find(LINK_TEXT_SELECTOR).send_keys(:enter)
+ end
+
def fill_in_link_url(url)
find(LINK_URL_SELECTOR).fill_in(with: url)
end
diff --git a/themes/horizon/javascripts/discourse/components/sidebar-new-topic-button.gjs b/themes/horizon/javascripts/discourse/components/sidebar-new-topic-button.gjs
index 25cdbb6101f7d..c168b64fe199f 100644
--- a/themes/horizon/javascripts/discourse/components/sidebar-new-topic-button.gjs
+++ b/themes/horizon/javascripts/discourse/components/sidebar-new-topic-button.gjs
@@ -5,7 +5,7 @@ import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
import { service } from "@ember/service";
-import { gt } from "truth-helpers";
+import { gt, not } from "truth-helpers";
import CreateTopicButton from "discourse/components/create-topic-button";
export default class SidebarNewTopicButton extends Component {
@@ -41,6 +41,30 @@ export default class SidebarNewTopicButton extends Component {
}
}
+ get tagRestricted() {
+ return this.tag?.staff;
+ }
+
+ get createTopicDisabled() {
+ return (
+ (this.category && !this.createTopicTargetCategory) ||
+ (this.tagRestricted && !this.currentUser.staff)
+ );
+ }
+
+ get categoryReadOnlyBanner() {
+ if (this.category && this.currentUser && this.createTopicDisabled) {
+ return this.category.read_only_banner;
+ }
+ }
+
+ get createTopicClass() {
+ const baseClasses = "btn-default sidebar-new-topic-button";
+ return this.categoryReadOnlyBanner
+ ? `${baseClasses} disabled`
+ : baseClasses;
+ }
+
@action
createNewTopic() {
this.composer.openNewTopic({ category: this.category, tags: this.tag?.id });
@@ -80,8 +104,10 @@ export default class SidebarNewTopicButton extends Component {
diff --git a/themes/horizon/scss/chat.scss b/themes/horizon/scss/chat.scss
index 960e50dbbd320..c3c7f5a63eb36 100644
--- a/themes/horizon/scss/chat.scss
+++ b/themes/horizon/scss/chat.scss
@@ -45,6 +45,19 @@ body.has-full-page-chat {
}
}
+// the below are elements whose z-index needs to be adjusted due to the above change
+.chat-message-actions-container {
+ .chat-drawer.is-expanded & {
+ z-index: calc(z("composer", "dropdown") + 1);
+ }
+}
+
+.fk-d-menu[data-identifier="usercard"] {
+ .has-drawer-chat & {
+ z-index: z("modal", "dialog");
+ }
+}
+
.chat-drawer {
.peek-mode-active & {
max-width: 90vw;
diff --git a/themes/horizon/spec/system/sidebar_topic_button_spec.rb b/themes/horizon/spec/system/sidebar_topic_button_spec.rb
index 54c2155e56566..875612401e759 100644
--- a/themes/horizon/spec/system/sidebar_topic_button_spec.rb
+++ b/themes/horizon/spec/system/sidebar_topic_button_spec.rb
@@ -34,10 +34,10 @@
expect(page).to have_css(".sidebar-new-topic-button__wrapper .topic-drafts-menu-trigger")
end
- it "does not disable button when visiting read-only category" do
+ it "disables button when visiting read-only category" do
visit("/c/#{private_category.slug}/#{private_category.id}")
- expect(page).to have_no_css(".sidebar-new-topic-button[disabled]")
+ expect(page).to have_css(".sidebar-new-topic-button[disabled]")
visit("/c/#{category.slug}/#{category.id}")