Two bugs:
1. The UI for Add link was over ambitious and over encoding / under
encoding URLs. The main fix is using URL here which we use elsewhere in
Discourse to do the encoding.
2. The Onebox system was encoding `+` which is a valid query param sub
component.
The IntersectionObserver callback is asynchronous, which means
`welcomeBannerSearchInViewport` remains `false` for a brief moment after
the welcome banner renders. This causes the header search to flash
briefly before being hidden.
Add a synchronous viewport check using `isElementInViewport()`
immediately after setting up the observer. This sets the correct initial
state before the first async callback fires.
Ref - t/171300
When a post is translated, we immediately replace the content based on
the user's language, but miss out updating the language indicator.
This commit fixes that.
The changes introduced in PR #36482 broke username
styling by swapping the content between `.username` and `.name` spans
based on the `prioritizeNameInUx` setting. This caused usernames to lose
their bold styling since CSS targets elements by class name, not
content.
This fix removes the custom AboutPageUser component and reuses the
existing UserInfo component instead. UserInfo handles name priority
correctly by using a `--name-first` CSS modifier class to change visual
order via flexbox, while keeping the username content in the `.username`
span where the bold styling is applied.
The page object for system specs is updated to detect which display mode
is active by checking for the `--name-first` class, then mapping the
span contents accordingly.
Ref - https://meta.discourse.org/t/391706
**BEFORE**
<img width="1510" height="1293" alt="2025-12-19 @ 11 36 13"
src="https://github.com/user-attachments/assets/56e1460e-30e9-41a4-92f7-c49bf9c3b9da"
/>
**AFTER**
<img width="1510" height="1293" alt="2025-12-19 @ 11 36 06"
src="https://github.com/user-attachments/assets/c92d165d-edaa-47e1-97fb-c6518ed3bbfe"
/>
All components (approx 57 instances) that use the following type of
setting in their `settings.yml`...
```
setting_name:
type: list
list_type: category
```
... will have the following warning shown.
```
Category.findById called with a string ID
```
This warning is not very useful, as it is already mitigated directly
below by doing a `id = parseInt(id, 10);`.
Originally introduced here:
https://github.com/discourse/discourse/pull/26342
Revised the deprecation handling to introduce a new `notify-admin`
handler, replacing the previous `CRITICAL_DEPRECATIONS` approach. This
ensures a more flexible and maintainable configuration while preserving
admin notifications for key deprecations.
Updated tests and validation logic to reflect the new `notify-admin`
behavior, including compatibility checks to prevent invalid handler
combinations such as `notify-admin` with `silence`. Also streamlined
handler naming by replacing `counter` with `count` for consistency.
PR makes two changes:
- replaces "English (US) with "English" everywhere
- in the language switcher dropdown, it replaces "English (UK)" with
"English" if that variant of English is the only one enabled
"English (US)" is needlessly geo-specific. Given it is the default
language, using "English" as the label is shorter and clearer. It still
differentiates from "English (UK)" when choosing the interface language
for users (or in admin UIs):
<img width="500" alt="CleanShot 2025-12-17 at 17 38 32@2x"
src="https://github.com/user-attachments/assets/abd7674c-2b72-4e5c-8543-050b7812c743"
/>
And the PR also strips `(UK)` from the language switcher list when
"English (UK)" is the only English variant enabled. This is a bit hacky,
but it lets us have cleaner UX (especially for countries like Canada,
Australia, where sites may opt for `en_GB` as their default) while
keeping the necessary technical separation between English and English
(UK) in admin and user preference screens.
---------
Co-authored-by: Natalie Tay <natalie.tay@gmail.com>
We were filtering directly a `ResultSet` instead of calling `content`
first.
This commit also adds test to cover the part of the listTags which was
incorrect and was not found when adding this deprecation.
This change enables local dates to render properly in poll options and
improves how dates are displayed in the rich editor.
What changed:
- Rich editor now displays localized, human-friendly dates (e.g., "Today
at 2:30 PM") instead of raw date strings ("2021-01-01 12:00:00") by
using LocalDateBuilder
- Poll options can now include local dates that are properly decorated
and interactive
- Added keyboard shortcut (Cmd/Ctrl+Shift+.) to insert current date
markup in poll option inputs
- Extracted reusable utilities: formatLocalDate for rendering and
generateCurrentDateMarkup for BBCode generation
How it works:
- New NULL_HELPER export in decorated-html provides a null-safe helper
object for contexts without a post/model, allowing HTML decorators to
work in poll options
- New Ember modifiers (decorate-cooked-content, decorate-poll-option)
apply decorators to poll content
- Tests updated to verify data attributes rather than text content,
since formatted dates vary by timezone
Internal ref - t/155024
**BEFORE/AFTER (markdown editor)**
<img width="1699" height="1327" alt="CleanShot 2025-12-16 at 11 59 22"
src="https://github.com/user-attachments/assets/ff9fbb2b-0f1b-41b7-b0fc-b59c73ed4e32"
/>
<img width="1699" height="1327" alt="CleanShot 2025-12-16 at 11 59 42"
src="https://github.com/user-attachments/assets/6b07beab-e864-4319-b97b-fe7abe2180e6"
/>
**BEFORE/AFTER (rich text editor)**
<img width="1699" height="1327" alt="CleanShot 2025-12-16 at 12 00 05"
src="https://github.com/user-attachments/assets/0f0e1a4b-0829-4e0e-94ac-90f16c4e52d5"
/>
<img width="1699" height="1327" alt="CleanShot 2025-12-16 at 11 59 52"
src="https://github.com/user-attachments/assets/d8db2e7a-c110-41bd-9fbf-b4af0f5de2c5"
/>
Plugins in the admin sidebar were displayed in an unpredictable order
based on load sequence. This makes it harder for admins to find specific
plugins, especially as the number of installed plugins grows.
Sort plugin links alphabetically by their translated display name, with
the internal plugin name as a secondary sort key for stability.
The sorting uses localeCompare for proper internationalization support.
Previously, plugins that needed to save user preferences on specific
preference pages had to either:
1. Manually register value transformers for
`preferences-save-attributes`
2. Use `modifyClass` to push fields to `saveAttrNames`
The second approach was broken because `saveAttrNames` is now a getter
that returns a fresh array each time, so pushed values were lost.
This commit introduces three new plugin APIs that handle the transformer
registration automatically:
- `addSaveableUserOption(name, { page })` - for `user_options` table
fields
- `addSaveableUserField(name, { page })` - for `user_fields` table
fields
- `addSaveableCustomFields(page)` - ensures `custom_fields` object is
saved on a page (auto-deduplicates across plugins)
The `{ page }` option specifies which preferences page triggers saving:
"account", "emails", "interface", "notifications", "profile",
"tracking", etc.
Also updates bundled plugins to use the new APIs:
- chat: `addSaveableUserOption` with `{ page: "emails" }` for email
frequency
- discourse-ai: `addSaveableUserOption` with `{ page: "interface" }`
- discourse-assign: `addSaveableUserOption` with `{ page: "tracking" }`,
`addSaveableCustomFields` with `"notifications"`
- discourse-policy: `addSaveableUserOption` with `{ page: "emails" }`
- discourse-rewind: `addSaveableUserOption` (no page, custom prefs page)
Deprecates `addSaveableUserOptionField` in favor of
`addSaveableUserOption`.
Ref - https://meta.discourse.org/t/391509
Follow up to ee1a1c7219
When moderators with `moderators_manage_categories` enabled change
category permissions, they could accidentally remove their own access,
requiring admin intervention to fix.
This adds a client-side confirmation dialog that warns moderators before
saving permission changes that would lock them out. If confirmed, the
changes are saved and the user is redirected to the home page.
Changes:
- Add `_wouldLoseAccess()` check in edit-category controller that
compares new permissions against the user's group memberships
- Show confirmation dialog via `dialog.yesNoConfirm()` before saving
- Redirect to home page after confirmed save (since user can no longer
access the category edit page)
- Fix `onSelectGroup` to properly set `group_id` for custom groups by
looking up from `site.groups` (was only working for auto groups)
Here's how the confirm dialog looks like
<img width="1606" height="1043" alt="CleanShot 2025-12-12 at 17 12 55"
src="https://github.com/user-attachments/assets/86e255dc-fa5c-4fde-8c86-3d3b0e03ce89"
/>
Ref - https://meta.discourse.org/t/390978
When "display name on posts" is enabled and "prioritize username in UX"
is disabled, quoting a user with quotation marks in their display name
(e.g., `John "The Dev" Smith`) breaks the quote markdown:
[quote="John "The Dev" Smith, post:1, topic:2"]
The BBCode parser's regex `"([^"]+)"` stops at the first `"` inside the
name, capturing only `John ` instead of the full name.
Alternatives considered:
- Backslash escaping (`\"`): Would require updating the parser regex to
support escape sequences and adding unescape logic. Adds complexity and
risks breaking existing quotes.
- URL encoding (`%22`): Requires decoding when rendering. Using
`decodeURIComponent` on user input creates XSS risk. A safe decoder that
only decodes specific characters adds complexity and attack surface for
minimal benefit.
The simplest solution is to strip quotation marks from names when
building the quote BBCode. This is safe (no user input decoding), simple
(no parser changes), and the minor cosmetic loss in the quote
attribution is an acceptable trade-off.
The `stripQuotationMarks` function is defined alongside the existing
`QUOTATION_MARKS` array in bbcode-block.js to keep related logic
together and avoid duplication.
Ref - https://meta.discourse.org/t/391153
What is the problem?
When a user does not have any tags configured to be displayed in the
sidebar, selecting the "selected" filter in the edit sidebar tags
navigation model was not displaying an empty state. Instead, it
displayed all the tags even though those tags were unselected.
The above behaviour is due to the fact that the client side was not
properly setting the `only_tags` param for the request to the
`/tags/list.json` endpoint
when the user has not selected any tags in the modal. As a result, the
endpoint ended up returning all the tags that are visible to the user.
What is the fix?
On the client side, we just have to avoid fetching tags when we already
know that the user has not selected any tags.
Followup 2fdc9af28e
We need to take into account the admin changing client
site settings, which sends a MessageBus evvent to the
client to update the siteSettings service. We need to
both:
* Update siteSettings.currentUserUpcomingChanges to keep it
in sync
* Use siteSettings[name] in upcomingChangeBodyClasses to
ensure we always have the latest calculated value for
the classes
This commit fixes both issues and adds a related system spec.
Refactor `PostStream` by replacing Ember's computed macros (`@or`,
`@and`, `@not`, `@equal`) with native getters and `@dependentKeyCompat`.
This modernizes the codebase to align with current Ember conventions and
improves readability and maintainability. No functional changes
introduced.
Previously we were not clearing title counter notification callbacks on
cleanup in test
this could lead to getting flakes (and progressively slower acceptance
tests) as we keep adding
adding more and more Title counters.
The title counter is an interface that lets plugins add counts to title.
Convert `findPostsByIds` and `loadIntoIdentityMap` to async functions in
`PostStream` to resolve a race condition when loading posts. This
ensures proper sequencing of asynchronous operations and improves
reliability in loading post data.
This PR addresses the issue reported in
https://meta.discourse.org/t/the-web-page-continuously-loads-previous-posts-and-automatically-scrolls-up-quickly/391192
There are no tests added because I can't find a way to reliably
reproduce the issue in tests and verify it's properly fixed.
Refactor `discourse-bootstrap.js` to use `importSync` when loading
`setupDeprecationCounter`, improving module dependency handling. This
ensures the code gracefully handles environments where the deprecation
counter module is unavailable.
This PR implements stricter deprecation handling that enforces
deprecation-free tests for core and preinstalled plugins, while allowing
custom (non-preinstalled) plugins and themes to have deprecations
without causing test failures.
### Key Changes
#### CI Workflow Improvements
- Split plugin system tests into separate CI targets: `core-plugins`,
`official-plugins`, and `chat`
- Enhance `bin/turbo_rspec` to accept comma-separated exclude patterns
via `--exclude-pattern`
- Simplify workflow configuration with default `shell: bash` and
consolidated environment variables
#### Plugin Classification & Detection
- Centralize official plugins list in `config/official_plugins.json` for
unified backend and frontend access
- Detect preinstalled plugins by checking for absence of `.git`
directory
- Add `isOfficial` and `isPreinstalled` metadata flags to plugin info
- Add `data-preinstalled` and `data-official` attributes to all plugin
and theme script tags for runtime identification
#### Deprecation Source Tracking
- Track deprecation sources (core, plugin, or theme) through template
map and resolver to attribute deprecations correctly
- Improve `source-identifier.js` to detect admin UI plugin files in both
development and production environments
- Add source information to deprecation messages for better debugging
#### Test Infrastructure
- Modify `raise-on-deprecation` test helper to skip errors for custom
(non-preinstalled) plugins and themes
- Add `EMBER_RAISE_ON_DEPRECATION` environment variable to control
deprecation throwing behavior in Rails tests
- Automatically set `EMBER_RAISE_ON_DEPRECATION` for core and
preinstalled plugin/theme specs in `rails_helper.rb`
- Improve deprecation summary output for system specs with test/spec
origin tracking
#### Deprecation Workflow Enhancements
- Add `dont-throw` handler for selective deprecation bypassing in test
fixtures without raising errors
- Add `dont-count` handler for preventing deprecation counting in
specific scenarios (e.g., test fixtures)
#### Deprecation Fixes
- Fix pending deprecations across core plugins (chat, data-explorer,
discourse-subscriptions, gamification, house-ads, reactions,
rss-polling, styleguide)
- Update import paths and remove deprecated patterns
- Migrate deprecated Handlebars templates to JavaScript API
### Testing Strategy
With these changes:
- **Core and preinstalled plugins** must pass all tests without any
deprecations
- **Custom plugins and themes** can have deprecations without failing
tests
- Test fixtures can use `dont-throw` and `dont-count` handlers when
testing deprecation behavior itself
- System specs automatically configure deprecation enforcement based on
test file location
---------
Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: Jarek Radosz <jradosz@gmail.com>
When clicking "Add to Google Calendar" for an event with a specific
timezone, the generated UTC times were incorrect. The code was parsing
the date string using the user's local timezone instead of the event's
timezone.
For example, an event at 18:30 in a UTC+2 timezone would be incorrectly
converted as if it were 18:30 in the user's local timezone (e.g.,
UTC-4), resulting in a 6-hour error in the Google Calendar URL.
The fix passes the event's timezone to `_formatDateForGoogleApi` and
uses `moment.tz()` to parse dates in the correct timezone before
converting to UTC. Also cleaned up the date formatting to use moment's
format() method instead of regex replacement.
Internal ref - t/162288
Whenever the `HorizontalOverflowNav` component is rendered, we call
`scrollIntoView` on the current active element to ensure that the link
which is currently active will be shown to the user. However, we were
calling `scrollIntoView` with the `container` option defaulting to `all`
which impacts all scrollable containers.
Per mozilla docs,
> All scrollable containers are impacted (including the viewport).
This is not ideal as it can cause unintended scrolls on the viewport
thus resulting in a bad UX. To resolve this, this commit adds the
`container:
"nearest"` option when we call `scrollIntoView` which ensures that only
the nearest scrollable container to the active element is impacted.
This problem was discovered while I was looking at review queue index
page and noticed that my scroll position was always lost when new
reviewable items are added to the page. It turns out that the review
queue index page renders the `reviewable-refresh/item.gjs` component
which then renders the `horizontal-overflow-nav.gjs` component.
Handle, timeout of user so we stop reading and scrolling when user is in
background or no longer active
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: David Taylor <david@taylorhq.com>