100 Commits
Author SHA1 Message Date
Martin Brennan 7bd148f6f9 UX: Improve UC for boards + voice (#43245)
Add related settings, fill out voice description based
on announcement topic

<img width="971" height="203" alt="image"
src="https://github.com/user-attachments/assets/a33ac3c8-25cf-4836-953a-f3f8416bc98c"
/>

<img width="970" height="200" alt="image"
src="https://github.com/user-attachments/assets/42b7b027-ce5e-44c1-9fd9-92fb7710de7a"
/>
2026-09-04 13:26:50 +10:00
Martin Brennan e22f9900d6 FIX: Create group staff action logging hole (#43153)
Add a staff action log for group creation, we don't have
that right now, only delete.
2026-09-03 16:19:25 +10:00
Martin Brennan 7dfd824b15 FIX: Refresh browse-more counts when topic tracking state changes (#43152)
The browse-more message was not consuming the tracking state’s
invalidation
token, so its new and unread counts remained stale after reading a
topic.
Recompute the message whenever the topic tracking state changes.
2026-09-03 12:07:02 +10:00
Martin Brennan 542b65070a DEV: Decrease RSpec startup time (#43151)
Reduces RSpec startup time by avoiding work that the selected spec does
not need.

The main changes are:

- Mark development/test dependencies as require: false where they can be
loaded safely at their point of use.
- Avoid loading Capybara, the Playwright driver, system helpers, and
page objects unless the selected specs need them.
- Autoload several large spec helper modules and load Rails tasks only
for task specs.
- Load OAuth providers, schema libraries, API documentation support, and
QR-code support at their actual call sites.
- Preload production dependencies before workers fork to preserve
copy-on-write sharing.
- Register parallel_tests Rake tasks only in local environments so
production bundles do not require test-only gems.
- Make rbtrace opt-in via RBTRACE=1.
- Avoid creating an unrelated top-level fixture in user_spec.rb.
- Explicitly load Capybara in the nginx integration spec, which sits
outside the system spec directory.

The new lazy support files work as follows:

- lazy_fabricators.rb indexes core fabricator definitions without
executing every file. When an unregistered fabricator is first
requested, only the file defining it is loaded. Plugin suites retain
eager fabricator loading for compatibility.
- lazy_faker.rb intercepts a missing Faker constant, loads the full
Faker gem on first use, and retries the lookup.
- lazy_pry.rb provides a temporary pry method that loads pry and
pry-rails on first use, then delegates to the real implementation.

On my machine, startup for bin/rspec spec/models/user_spec.rb:20
decreased from approximately 2.1 seconds to 1.1 seconds—a reduction of
roughly 47%.
2026-09-03 11:41:29 +10:00
Martin Brennan d2bbcd5f6d Revert "DEV: Decrease rspec startup time (#42946)" (#43147)
This reverts commit 8b8dbf5bae.
2026-09-03 10:59:58 +10:00
Martin Brennan 8b8dbf5bae DEV: Decrease rspec startup time (#42946)
Reduces RSpec startup time by avoiding work that the selected spec does
not need.

The main changes are:

- Mark development/test dependencies as require: false where they can be
loaded safely at their point of use.
- Avoid loading Capybara, the Playwright driver, system helpers, and
page objects unless system or request specs need them.
- Autoload several large spec helper modules and load Rails tasks only
for task specs.
- Load OAuth providers, schema libraries, API documentation support, and
QR-code support at their actual call sites.
- Make rbtrace opt-in via RBTRACE=1.
- Avoid creating an unrelated top-level fixture in user_spec.rb.
 
The new lazy support files work as follows:

- lazy_fabricators.rb indexes core fabricator definitions without
executing every file. When an unregistered fabricator is first
requested, only the file defining it is loaded. Plugin suites retain
eager fabricator loading for compatibility.
- lazy_faker.rb intercepts a missing Faker constant, loads the full
Faker gem on first use, and retries the lookup.
- lazy_pry.rb provides a temporary pry method that loads pry and
pry-rails on first use, then delegates to the real implementation.

On my machine, startup for bin/rspec spec/models/user_spec.rb:20
decreased from approximately 2.1 seconds to 1.1 seconds—a reduction of
roughly 47%.
2026-09-03 10:11:50 +10:00
Martin Brennan 5920231ef0 FIX: Load boards plugin SVG icons lazily (#43112)
Boards queried the database during startup to register user-selected
icons, breaking database-less tasks such as asset precompilation. It
also caused stale per-process state and could leak icons between sites.

Load plugin icon sources when building the site-scoped SVG sprite
instead.
2026-09-03 10:11:25 +10:00
Martin Brennan ddb93afd02 DEV: Remove old kanban theme component from popular themes list (#43114)
Also remove other random refs
2026-09-03 10:03:41 +10:00
Martin Brennan 2ff9ea5649 UX: Fix boards plugin description & learn more topic ID (#43113)
Was pointing to the old TC topic on meta, also add
sidebar icon for the plugin
2026-09-02 14:33:17 +10:00
Martin Brennan 5a952ebcfe DEV: Fix asset compilation error for boards plugin (#43110)
Issue is that plugins/boards/plugin.rb:46 hits the database at
after_initialize (data_source_exists?(:discourse_kanban_columns)) to
pre-register column icons. The Publish Assets job runs rake
assets:precompile with no Postgres container, so the connection fails
and boot aborts.

The rescue there was meant to cover this but misses the actual class:
PG's failure surfaces as ActiveRecord::ConnectionNotEstablished. The
listed classes are NoDatabaseError, StatementInvalid, and
DatabaseConnectionError — and DatabaseConnectionError is a subclass of
ConnectionNotEstablished, not its parent, so it doesn't catch it.

The fix is to widen the rescue to ActiveRecord::ConnectionNotEstablished
(covers NoDatabaseError and DatabaseConnectionError) alongside
StatementInvalid
2026-09-02 13:34:08 +10:00
Martin Brennan 1d7ede7216 DEV: Move discourse-kanban into core as boards (#43056)
Moves the https://github.com/discourse/discourse-kanban plugin into
core as `boards`, with `Boards` as the engine const.

Table names are unchanged, but setting names are changed and
we create new setting records in a migration, along with duplicating
ACL rows with the new `Boards::Board` target class.
2026-09-02 13:10:20 +10:00
Martin Brennan 5f72eb0203 DEV: Minor fixes for granular_anonymous_and_logged_in_groups_permissions upcoming change (#43063)
The following fixes are made here to make the transition to
`granular_anonymous_and_logged_in_groups_permissions` being
permanently enabled easier for people.

- **DEV: Show logged_in_users and anonymous_users in setting list**
- Previously, these groups would only show if
`granular_anonymous_and_logged_in_groups_permissions` is enabled,
but they are usable now everywhere in core, and this will help ease the
transition for themes + settings.
- **DEV: Add everyone -> logged_in_users map for theme settings**
- Theme settings now copy the same pattern as site settings, where
previously selected `everyone`
is dynamically changed to `logged_in_users` when
`granular_anonymous_and_logged_in_groups_permissions`
     is enabled.
- **DEV: Show everyone group as everyone (legacy) in site setting group
list pickers**
  - Makes it clearer this group will soon not be supported
- **DEV: Allow anonymous_users in AnonymousUser#in_any_groups?**
- Anon users should return true for `in_any_groups?` for
`anonymous_users` regardless of whether
`granular_anonymous_and_logged_in_groups_permissions` is on
2026-09-02 10:44:52 +10:00
Martin BrennanandPenar Musaraj 0ed4a27ef0 DEV: Add upcoming change at Experimental for voice plugin (#43053)
Adds the upcoming change and a basic screenshot, feel
free to change anything about this:

<img width="752" height="407" alt="image"
src="https://github.com/user-attachments/assets/b088d17c-87c0-472c-a343-08ed6095fbf6"
/>

<img width="978" height="142" alt="image"
src="https://github.com/user-attachments/assets/77c4e7fc-43de-4fe4-a986-e9163086e24f"
/>

The linked meta topic needs to be published on merge.

---------

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
2026-09-01 16:54:24 -03:00
Martin Brennan 1ab9b6d12a DEV: Allow arguments for close() and onClose in DMenu/FloatKit (#43038)
In some situations, you want to have something like a submenu
where the child menu can close either from:

* The user moving their mouse away from the submenu or
* The user clicking on an item in the submenu

In this case, if the parent has an onClose handler, and
some logic to close itself when the child closes, you
want to be able to pass an argument from the child to
the parent in case the parent should only close
if the child closes from a click, not just because the
mouse moved out of the child submenu.

Then you can do something like:

```
onClose(data) {
  if (data?.reason === 'saved') {
    this.close();
  }
}
```
2026-08-31 15:46:40 +10:00
Martin Brennan 49dbb2c544 DEV: Lint symlinked plugins using relative paths (#43037)
ExternalLinter runs from the plugin root, but previously passed absolute
file paths through the symlink under discourse/plugins. RuboCop expands
exclusion patterns relative to the plugin's real root, so those paths do
not match.

For example, discourse-kanban system specs incorrectly triggered
RSpec/DescribeClass even though spec/system is excluded. Pass file paths
relative to the plugin root so linter configuration matches symlinked
plugins correctly.
2026-08-31 14:52:29 +10:00
Martin Brennan a84ecf92e4 DEV: Disallow everyone from ai_bot_allowed_groups (#42944)
Also fix one call site where user.in_any_groups? was not used
2026-08-31 13:55:01 +10:00
Martin Brennan 54e3c2a563 FIX: Do not close modal when DMenu closes (#43035)
Fixes an issue found in the DAccessControl component inside
a modal (in the kanban plugin). Selecting an option from the
permission dropdown for an ACL row on mobile would close the
whole modal. Now, this only happens when `@modalForMobile={{true}}`
is used on a DMenu outside a modal.
2026-08-31 13:54:19 +10:00
Martin Brennan c1e14963b9 DEV: Deprecate calling user.groups on client directly (#42711)
Renaming this array to user.visibleGroups/currentUser.visibleGroups to
make it clear that this is not the full list of groups a user belongs
to, but only the ones that are visible to the current user.

Adds a `deprecated` warning when calling `user.groups` on the client,
and
adds a `user.visibleGroups` alias to the new property.
2026-08-31 11:05:47 +10:00
Martin Brennan 1e0690897b FIX: Error when group has no full name in group user chooser (#42994)
Handles an error in the group finder for the ACL editor
when a group has no full name, and fixes the display of
groups with no full name in the dropdown results.

<img width="1920" height="730" alt="image"
src="https://github.com/user-attachments/assets/920da06b-39ee-4ac5-89bd-d25c0a1a623d"
/>
2026-08-28 12:19:49 +10:00
Martin Brennan 6c296e05ff DEV: Add PLAYWRIGHT_DEVTOOLS env var to automatically show devtools in chrome (#42973)
Similar to the old CHROME_DEVTOOLS="POSITION" command,
this automatically opens the devtools to the right,
you can't control the position in playwright.
2026-08-28 11:36:18 +10:00
Martin Brennan 43cce00681 FEATURE: Bump dismiss button & new checkbox upcoming changes to permanent (#42871)
These have been stable for a long time and are mostly cosmetic,
bumping to permanent, removing code branches, and incorporating
CSS changes into the main stylesheets.
2026-08-27 09:28:29 +10:00
Martin Brennan 9092c8e5ac DEV: Fix AI logs page system spec period selection (#42832) 2026-08-24 11:44:04 +10:00
Martin Brennan 6965bdb69f FIX: Unable to join Zoom on multi-day livestream event (#42754)
These are not very common, but if there was a multi-day livestream
event with Zoom, we would show the "You can join the webinar closer
to the event start time message" even if the current time was within
the multiple days.

Now we will only show this message before the grace period before
the start time of the first day of the event.

<img width="476" height="453" alt="image"
src="https://github.com/user-attachments/assets/ce575fbd-9a88-4bc4-a1b2-ba867da8113e"
/>
2026-08-24 10:36:12 +10:00
Martin Brennan 34804d3a74 UX: Further followup of AI logs filtering (#42763)
Followup 4b550140c8

Make some more changes to DFilterControls to support additional
filters needed by the AI logs page, and remove more bespoke
form/filter code from that page.

<img width="1015" height="619" alt="image"
src="https://github.com/user-attachments/assets/6cc9c081-346b-4534-9b22-0bc8c65024d9"
/>
2026-08-24 09:38:48 +10:00
Martin Brennan 5473f1e56f FIX: Giant YouTube fallback to iframe in chat (#42758)
Fixes the root cause of the giant YouTube videos in chat,
when the iframe is used instead of lazy video (such as in
cases where the URL is a weird format or the video is for
some reason not available and returns a 401).

Gives the iframe video the same max-width as lazy video/
images, and changes the title to YouTube instead of the
URL to the generic youtube thumbnail.

**Before**

<img width="1872" height="703" alt="image"
src="https://github.com/user-attachments/assets/219977d6-e984-4a3c-aa1b-780a3ba3f49d"
/>


**After**

<img width="786" height="370" alt="image"
src="https://github.com/user-attachments/assets/fdbb47ae-91dd-4e59-aeb7-6dc7b227132f"
/>
2026-08-20 17:16:59 +10:00
Martin Brennan b828735c77 FIX: Allow YouTube URLs with a list= query param to use lazy-video (#42706)
YouTube links like
https://www.youtube.com/watch?v=IYH7_GzP4Tg&list=RDIYH7_GzP4Tg&start_radio=1
where not being oneboxed by lazy-video because it explicitly excluded
URLs with a list= query param. This commit removes that exclusion,
allowing
lazy-video to onebox YouTube links with a list= query param.

This is especially noticable in chat, where before this would change
one of those videos would be absolutely enormous and take up the full
screen.

Also slightly increases height of videos in chat to 250px, and adds
margin top so the collapser link isn't right up against the top of the
video.

**Before**

<img width="2203" height="802" alt="image"
src="https://github.com/user-attachments/assets/a6bbe15e-51fb-4370-b9af-688a312990db"
/>

**After**

<img width="1072" height="375" alt="image"
src="https://github.com/user-attachments/assets/1582b369-2663-45b5-b55f-96e6c204d971"
/>
2026-08-19 14:33:37 +10:00
Martin Brennan e053c4caac DEV: Add prioritizeRecentTags option to TagChooser (#42704)
Regis already added this in 62bc006ea6 to
the MiniTagChooser, so this commit brings parity, and also adds
the same option to be passed down via FKControlTagChooser
2026-08-19 11:29:25 +10:00
Martin Brennan 61005d37c0 UX: Further fix twitter oneboxes in chat (#42666)
Followup 631bb1c6c7

I missed some CSS styles yesterday, also move the classes to the
file where the other chat onebox overrides are
2026-08-18 16:19:53 +10:00
Martin Brennan 6e9c3c4d39 FEATURE: Add ProseMirror tab indent/outdent support for code blocks (#42462)
Brings a feature of the old markdown editor code blocks
to the ProseMirror RTE code blocks. You can select a portion
of code in a code block and do Tab to indent it and Shift+Tab
to outdent it, which makes copying code + formatting it in
the editor a lot nicer. Ctrl+Z works with this too.



https://github.com/user-attachments/assets/f09cd3f9-e106-451e-955d-4b605b41cd75
2026-08-18 12:07:11 +10:00
Martin Brennan 631bb1c6c7 UX: Improvements to twitter oneboxes in chat (#42644)
* Stop showing the tweet at 100% width, its way too wide and unnecessary
* Fix spacing/alignment for user avatar + username
* Improve image and video heights and widths

**Before**

<img width="2263" height="421" alt="image"
src="https://github.com/user-attachments/assets/ca101793-2484-4a7f-bfb4-ff5fdb3736bc"
/>

<img width="2258" height="538" alt="image"
src="https://github.com/user-attachments/assets/60201877-ee42-49a5-8eee-58583c792c71"
/>

<img width="2238" height="368" alt="image"
src="https://github.com/user-attachments/assets/86f1478f-9bfc-4874-8ae4-c0c23ebf29c8"
/>

<img width="1887" height="373" alt="image"
src="https://github.com/user-attachments/assets/3566074b-d818-4ee2-84e3-c8b080271a52"
/>


**After**

<img width="746" height="686" alt="image"
src="https://github.com/user-attachments/assets/0f362b9a-7d7c-4d30-ab2a-81f28d7b62f6"
/>

<img width="593" height="698" alt="image"
src="https://github.com/user-attachments/assets/09ff8ea5-f957-4bf9-8b0c-1401be0b176c"
/>

<img width="554" height="587" alt="image"
src="https://github.com/user-attachments/assets/7073164f-c429-4a77-8f6b-2b80b9eeebaf"
/>

<img width="558" height="588" alt="image"
src="https://github.com/user-attachments/assets/6f43e1d6-f323-4f86-9103-644fe5b3d25f"
/>
2026-08-18 09:20:53 +10:00
Martin Brennan 88c3fe6ee0 UX: Do not center reddit onebox in chat message (#42641)
This takes up way too much space, everything else
is over to the left in chat too. Give a bit more spacing
on top as well.

**Before**

<img width="1080" height="604" alt="image"
src="https://github.com/user-attachments/assets/c16e8a9a-8e47-4468-be04-11bb75f82bd9"
/>


**After**

<img width="1066" height="618" alt="image"
src="https://github.com/user-attachments/assets/cf6b27e2-d098-4695-be79-9842df7d2353"
/>
2026-08-17 15:59:37 +10:00
Martin Brennan 699ad46536 FIX: Handle new pseudogroups in message bus group IDs (#42610)
For logged in users, MessageBus needs to be able to handle
publish directives with group_ids including the new
anonymous_users and logged_in_users pseudogroups. This commit updates
the
message bus configuration to recognize these pseudogroups and ensures
that
they are correctly processed when publishing messages to the bus.

If granular_anonymous_and_logged_in_groups_permissions is disabled,
then the everyone pseudogroup will also be used for logged in users.

The use case for this was Kanban boards publishing card updates,
some board permissions have logged_in_users (group ID 5) in the board
permissions which we use for MessageBus group_ids.
2026-08-17 09:35:37 +10:00
Martin Brennan 2d1c4701e6 DEV: Disallow anonymous_users group for styleguide_allowed_groups (#42571)
Not much point in allowing this, and I doubt this is the case
anywhere in the wild. I also doubt more than a handful of sites
even have this plugin enabled, it's more of a developer tool.
2026-08-14 14:43:04 +10:00
Martin Brennan af961f28c7 FIX: Handle no ACL permissions on create (#42569)
Followup a0de88db7b

This extends the /evaluate endpoint for ACLs to handle the case
where, when creating something like a kanban board, the user
would end up with insufficient permissions (determined by
the ACL target) to further access the new entity.

For example, if a regular user can create a kanban board,
but they do not add themselves or a group they are in to
the Manage permissions ACL, then they will not be able to access
the board after creation.
2026-08-14 06:59:44 +10:00
Martin Brennan 2ed066afc7 FEATURE: Add fields to the schedule workflow node payload (#42573)
Adds the following schedule payload items:

* date, which is the date of the schedule run in YYYY-MM-DD
* month_number, which is the month number of the schedule run (1-12)
* time, which is the combined 24 hour time of the schedule run in
HH:MM:SS format

This can be helpful when using the schedule data to get e.g.
a week/month/time that adds or subtracts from the schedule run
date/time.
2026-08-13 09:54:15 +02:00
Martin Brennan d212490c66 FEATURE: Make discourse-reactions enabled by default (#42564)
Makes the enable_discourse_reactions_by_default upcoming change
permanent, meaning the new default of true is now permanent
2026-08-13 12:44:39 +10:00
Martin Brennan a0de88db7b FEATURE: Evaluate ACL before save to prevent user from removing themselves from ACL (#42044)
It's currently very easy to lose your permission to an ACL target
by removing permissions, for example here if you're part of the
Pals group and not an admin, you can remove Pals and lose access
to e.g. the kanban board:

<img width="629" height="694" alt="image"
src="https://github.com/user-attachments/assets/9465d4a6-603d-421c-aaef-23eb3724083c"
/>

This PR adds an `/access-control/evaluate` endpoint and a FormKit
field for `DAccessControl` (called `DAccessControlField`) that performs
a validation using the new
ACL proposed and the current user's groups and user ID to check
if they will still have access after save. If not, we show this
confirmation
dialog:

<img width="1107" height="738" alt="image"
src="https://github.com/user-attachments/assets/b52d95e2-91f9-4227-b119-fda813339fd1"
/>

In addition to a general "no access left" check, kanban targets can
define
a `loss_warning_permissions` class method that says "if these
permissions would be lost,
warn the user", defining the error message to show under server.en.yml
in

`access_control_list.errors.#{target_class.acl_target_key}_user_will_lose_permission`

If you press OK then we save and you lose your access. If you press
Cancel,
you can reconfigure the ACL before trying again.

Also adds a `preventSubmit` function on FormKit when validating a field.
Instead of calling `addError` which shows an error message,
`preventSubmit`
can stop the field from proceeding if there is e.g. a validation that
requires
user confirmation.
2026-08-13 09:57:40 +10:00
Martin Brennan ba49af106e UX: Add shortcut to add details block in RTE (#42496)
This will be Cmd+Shift+D on Mac and Ctrl+Shift+D on Windows/Linux.
It will insert a details block at the current cursor position in the
RTE,
and the shortcut shows in the (+) menu.
2026-08-11 15:14:00 +10:00
Martin Brennan 7b2a2d5fde FIX: Zoom webinar livestream not joining on countdown end sometimes (#42391)
**Disclaimer: Claude was the driver here mostly, but I tested
before/after locally**

Attempts to fix some issues with the Zoom livestream integration
when a user clicks Join before the webinar has started by the host.
This shows a 30s countdown timer and then attempts to join the webinar
when the countdown ends.

However, when the tab wasn't focused/visible, Chrome (and assuming
other browsers) do some throttling of timers, requestAnimationFrame,
and ResizeObserver callbacks. When the user switches back to the tab,
the Zoom video frame appeared to not be rendered.

The Zoom frame was `display: none` when `init()` measured it, the
desktop rule setting that had a higher specificity than the `--visible`
class meant to reveal it, so the class never applied and the element
only became visible once Zoom wrote an inline `display` of its own.
`computeZoomViewSize` read a zero width and clamped to its 240px
minimum, meaning every session configured Zoom at the wrong size.

That went unnoticed because the post-join sync corrected it
immediately. But that sync runs in a frame callback, and both it and
the modifier's resize observer are suspended while the document is
hidden.

A retry that landed in a background tab never got the
correction, and the single queued frame callback fired on return
racing Zoom's remount, so the meeting rendered at 240px with none of
the layout heights applied, making it seem like the video didn't appear.

Moves the base `display: none` to where `--visible` outranks it, so
the frame is laid out before Zoom is initialized; falls back to the
container's width rather than the minimum when the root measures zero;
and skips the frame callbacks while hidden, re-running them on
`visibilitychange` instead.
2026-08-07 07:56:52 +10:00
Martin Brennan 7ccf6d44eb DEV: Add helper methods for site.js for group access (#42337)
Adds groupName and groupFullName helper methods to site
model, this is something we commonly use in the codebase,
and this lets us avoid calling groupsById[id]?.name needing
safe navigation.
2026-08-05 17:04:02 +10:00
Martin Brennan 57779cc4be FIX: can_create_discourse_post_event? not checking user groups correctly (#42284)
This commit fixes `can_create_discourse_post_event?` manually checking
a user's groups, which doesn't account for pseudogroups like
`logged_in_users`. We should always use `user.in_any_groups?` for this.

In addition, the opportunity is taken here for some cleanup, moving
all the guardian extensions from plugin.rb into a proper
`GuardianExtensions` file for calendar, adding extra specs along the
way.

Finally, I've added a `Group.refresh_automatic_groups_for_user!`
method as a single-user variant of `Group.refresh_automatic_groups!`
that can use in fabricators for testing, to make this more
thorough/reliable
than manually adding group user records in `after_create` in the
fabricators
2026-08-04 16:09:50 +10:00
Martin Brennan f58515dbb8 DEV: Return false not nil for Acl::Target group + user checks (#42291)
The methods group_has_permission? and user_has_permission? were
returning nil, not false, if the entity did not have permission even
though they are framed as questions. Let's return false as expected,
which is also what we do for the multiple versions of these which
are group_has_any_permission? and user_has_any_permission?
2026-08-04 15:36:14 +10:00
Martin Brennan d798c84585 DEV: Fix const redefine warning in topic voting (#42290)
Fixes this console error:

```
 [rails] /Users/mb/repos/discourse/plugins/discourse-topic-voting/lib/discourse_topic_voting/topic_query_extension.rb:4: warning: already initialized constant DiscourseTopicVoting::TRENDING_SCORE_SQL
  [rails] /Users/mb/repos/discourse/plugins/discourse-topic-voting/lib/discourse_topic_voting/topic_query_extension.rb:4: warning: previous definition of TRENDING_SCORE_SQL was here
```
2026-08-04 14:13:13 +10:00
Martin Brennan 982fba1536 FIX: Add missing solved_topics_auto_close_hours custom field entries (#42255)
When `solved_topics_auto_close_hours` is null/missing in the DB for
Support type categories, the UI defaults to 48 hours for this setting,
but this can be surprising for admins when changing some other support
category setting, as it will go from 0 -> 48 hours in the background.

This commit fixes the issue by backfilling a new custom field
value for `solved_topics_auto_close_hours`, setting it to `0`
on categories with `enable_accepted_answers` set to true
and no existing value for the field.

New support categories are unaffected, as we set 48 hours by
default there.
2026-08-04 09:04:44 +10:00
Martin Brennan 4de06a006b FIX: Do not notify new sites or plugins enabled of old upcoming changes (#42045)
For upcoming changes, we were having a problem where we
were notifying admins of upcoming changes in brand new sites.
A previous fix 2e5afb46e2f5c0547bc298b2bd1a9c39e813dc17 tried
to remedy this, but since it relies on the `new_site?` method, which
isn't true after 1 hour, the next time the scheduled notifications are
run for an admin they will be sent them even if their site is relatively
new.

This commit attempts to conclusively fix the issue by inserting
upcoming change `admins_notified_automatic_promotion` and
`admins_notified_available_change` events via `BackfillNotifiedEvents`
for changes that exist in particular statuses when the site is first
created. These events will have `event_data: {"backfilled" => true}`
to distinguish them from others.

It also fixes an adjacent issue where an admin was notified of a
plugin's upcoming changes as soon as they enable the plugin,
which isn't useful as the intent of upcoming changes is to notify
of changes for things the admin has already been using.

**Before this change, on brand new site with current upcoming change
state**

<img width="401" height="429" alt="image"
src="https://github.com/user-attachments/assets/a89cd641-59aa-4c48-a57c-9692021df176"
/>

**After this change**

No notification to show :) But when I moved an upcoming change to `beta`
it showed
the notification for the enabled upcoming change correctly:

<img width="378" height="174" alt="image"
src="https://github.com/user-attachments/assets/20e4096f-e83f-4c06-970d-a9b43561b12e"
/>
2026-07-30 09:51:08 +10:00
Martin Brennanandchapoi c84202174b FIX: Chat browse page on mobile (#42115)
Followup 4c7a4f637f

Fixes the broken chat browse page on mobile, and moves
the filters to this page to use DFilterControls component.

Removed a lot of custom CSS and HTML.

Also added arguments to DFilterControls to:

* Hide the no results message
* Force the toggle filter button, even if there aren't
  more than 1 dropdown filters, which can be useful on mobile

**Before**

<img width="1195" height="674" alt="image"
src="https://github.com/user-attachments/assets/417fad66-c045-43c7-b469-9f226678bdb2"
/>

<img width="383" height="675" alt="image"
src="https://github.com/user-attachments/assets/3d2d458b-b335-4702-88d7-733f9152940d"
/>


**After**

<img width="1208" height="589" alt="image"
src="https://github.com/user-attachments/assets/0353b6f5-c7af-4c67-82a8-87226dc9496a"
/>
<img width="424" height="707" alt="image"
src="https://github.com/user-attachments/assets/702c8b35-fc43-45b6-a8ca-2d0a8066458c"
/>

---------

Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com>
2026-07-30 09:50:23 +10:00
Martin Brennan c21f997feb FEATURE: Promote Ideas & Events category types to permanent status (#42080)
These have been Stable for ages now
2026-07-29 09:20:40 +10:00
Martin Brennan 361869df7c SECURITY: Stop exposing hidden tag names through category serializers 2026-07-28 16:40:22 +02:00
Martin Brennan e1b647229a SECURITY: Prevent cache poisoning/XSS via color scheme cookies 2026-07-28 16:40:22 +02:00
Martin Brennan 8295113dc9 UX: More Zoom livestream UI issues (#41824)
* Do not show the direct Zoom link when the livestream
is enabled for the event, we want to keep people on the
Discourse site

<img width="942" height="637" alt="image"
src="https://github.com/user-attachments/assets/6267ce8d-7848-4a7d-8769-78f82dd87417"
/>

* Prompt anonymous users to log in when Join Zoom is clicked
2026-07-27 15:31:41 +10:00
Martin Brennan d5c4e6ffe8 PERF: Minor site setting & UC related changes (#41961)
* Prevent unnecessary Hash#merge in DefaultsProvider#all_clean,
  Fixes an unnecessary merge which can sometimes cause performance
  overhead when calling SiteSetting.defaults_provider.all_clean.
  We don't need to merge the defaults when locale provided is the
  same as the default locale
* Do not check can_lazy_load_categories? in loop,
  This is a small perf optimization, we don't need to check
  can_lazy_load_categories? for every category in the site,
  which looks at the user's groups and upcoming changes every
  time, we just need to do it once.
2026-07-27 11:05:39 +10:00
Martin Brennan 9e9de80295 UX: Better admin emoji page sorting & filtering and service refactors (#41892)
This commit uses `DFilterControls` for emoji list filtering, and sorts
the
emoji by group then by name, with default always at the top:

<img width="1101" height="803" alt="image"
src="https://github.com/user-attachments/assets/77e384c1-33b1-4741-bd55-ca86cd5f2a03"
/>

I've also refactored the create + delete endpoints into services.
2026-07-24 12:01:21 +10:00
Martin Brennan ad7dbac77b DEV: Add permanent_warning: false to more upcoming changes (#42011)
These changes don't need to show the scary warning to
admins when they become stable, as they are still optional
features even when the upcoming change is gone
2026-07-24 11:42:36 +10:00
Martin Brennan 6fd9ae05e6 DEV: Fix upcoming change logging for NotifyPromotion (#41967)
Followup e66d6c56e2

I was referring to `result` outside the block, and also realised
that should_notify_admins policy was not being checked, which is
why we were ending up with the N/A error in the first place
2026-07-24 11:33:29 +10:00
Martin Brennan 34946bef21 FIX: Hidden setting error when enabling plugin linked to UC (#42007)
The `enable_discourse_workflows` site setting is both the plugin enabled
setting AND an upcoming change, which are always hidden settings.

Admins, when enabling the plugin, would see an error like this
since the setting was hidden:

> You are not allowed to change hidden settings:
enable_discourse_workflows

This commit fixes this by allowing hidden settings to be changed if the
setting is an upcoming change
2026-07-24 09:58:37 +10:00
Martin BrennanandLoïc Guitaut ea74b65aa5 DEV: Preserve existing errors in service model steps (#41896)
`ModelStep` revalidates changed models by calling `invalid?`, which
clears errors
added by the model producer before validations run. For example,
`UploadCreator`
can return an unsaved `Upload` containing an image-processing error;
`ModelStep`
then clears that error and may treat the `Upload` as valid.

Treat models that already contain errors as invalid without revalidating
them.
This preserves operation-specific errors for `on_model_errors` while
continuing
to validate changed models that do not already contain errors.

---------

Co-authored-by: Loïc Guitaut <loic@discourse.org>
2026-07-23 17:15:26 +10:00
Martin Brennan e66d6c56e2 DEV: Better logging of unexpected UC notification error (#41956)
This kind of error and backtrace that is unexpected and
has no error backtrace info is happening for some sites in
production, but I can't tell why:

> [CheckUpcomingChanges (SITE)] Failed to notify about promotion of
'enable_simplified_category_creation': with backtrace: N/A

Add more logging to try get a better reason
2026-07-23 10:51:22 +10:00
Martin Brennan b540fc6854 UX: Disable ACL grantee row when all permissions banned (#41865)
When we define banned_acls on an ACL target, there is a
possible case where all permission types for that ACL target
are banned for a particular grantee (group/user). For example,
if you do not want anonymous_users to view, edit, or manage
a certain target.

In this case, instead of hiding the grantee row in the chooser we
will disable it and show a message to the user so it's clear why it's
disabled

<img width="687" height="614" alt="image"
src="https://github.com/user-attachments/assets/d8aa60c6-a2f1-4821-b715-55595ec0a03d"
/>
2026-07-23 10:44:42 +10:00
Martin Brennan f36388234b FIX: Upcoming change problem check not respecting should_display? (#41955)
Our UpcomingChangeStableOptedOut problem check was showing
problems on brand new sites, when it should only show a
problem after admin has opted out of a stable upcoming change.

This was happening because on some sites a plugin might be disabled
or not configurable, so it would return false when calling
`SiteSetting.send(change_name)`, but crucially we weren't filtering
out upcoming changes that shouldn't display on the site.

This commit fixes the issue by returning no problem if the upcoming
change is not configured to display on the site.
2026-07-23 10:22:00 +10:00
Martin Brennan b736e72b1c DEV: Retroactive review and refactor of custom emoji bulk import/export (#41860)
Followup bd3906ce64

Converts the new endpoints made in the original PR into
service classes, using AI to drive implementation based
on the service authoring skill:

* CustomEmoji::Export
* CustomEmoji::PreviewImport
* CustomEmoji::ConfirmImport

In addition, some supporting action, lib, and serializer
classes have been added. This fixed a couple of
bugs as well:

* Import names now go through `Emoji.sanitize_emoji_name` like
single-emoji create
* No-group imports no longer persist a literal "default" string
* Two 500s (suspicious zip entries, malformed CSV) became proper 422s
* Duplicate CSV names are flagged at preview instead of exploding the
confirm transaction
*` Emoji.clear_cache `moved outside the transaction
2026-07-22 10:06:54 +10:00
Martin Brennan 19c97e0f89 UX: Unhide group name in ACL grantee chooser when different from full name (#41863)
Partial followup of 7ff30d81a3

The previous commit totally hid the group name
in the DAccessCopntrol grantee chooser (derived from
EmailGroupUserChooser) in favour of full name.

However in some cases it is useful to see the group name
as it may be different from the full name. This commit brings
back the group name after the full name, and adds an option
to not show the name if it's the same as the full name.

<img width="680" height="760" alt="image"
src="https://github.com/user-attachments/assets/127187fe-4e23-4689-a97d-f7e65870a353"
/>
2026-07-21 15:56:11 +10:00
Martin Brennan 67d2410919 DEV: Fix UC status report when reverted files encountered (#41821)
Fixes the following error when generating the upcoming changes status
report
on a weekly basis in GitHub actions:

```
/__w/discourse/discourse/lib/upcoming_changes/status_report.rb:56:in 'UpcomingChanges::StatusReport::Git#capture': git show 45535887f1231b40f8a21b8c154e58cef8670063:plugins/discourse-workflows/config/settings.yml failed: fatal: path 'plugins/discourse-workflows/config/settings.yml' exists on disk, but not in '45535887f1231b40f8a21b8c154e58cef8670063' (RuntimeError)
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:47:in 'UpcomingChanges::StatusReport::Git#show_file'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:227:in 'UpcomingChanges::StatusReport::GitHistory#statuses_at'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:215:in 'block in UpcomingChanges::StatusReport::GitHistory#add_history_for_settings_file'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:214:in 'Array#reverse_each'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:214:in 'UpcomingChanges::StatusReport::GitHistory#add_history_for_settings_file'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:205:in 'block in UpcomingChanges::StatusReport::GitHistory#by_change'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:202:in 'Hash#each'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:202:in 'Enumerable#each_with_object'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:202:in 'UpcomingChanges::StatusReport::GitHistory#by_change'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:422:in 'UpcomingChanges::StatusReport#history_by_change'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:363:in 'block in UpcomingChanges::StatusReport#report'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:362:in 'Hash#each'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:362:in 'Enumerable#map'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:362:in 'UpcomingChanges::StatusReport#report'
	from /__w/discourse/discourse/lib/upcoming_changes/status_report.rb:499:in 'UpcomingChanges::StatusReport::CLI.run'
	from /__w/discourse/discourse/script/upcoming_changes_status_report:9:in '<main>'
```

This happened because we were trying to run `git sho` on a file that was
reverted in a commit. Now, we check whether the file exists for the
specific commit hash before running git show, and continue with the
next valid commit for the file.
2026-07-20 16:37:35 +10:00
Martin Brennan fc8df41adb DEV: Add disallowed_groups to theme/component settings (#41792)
Further following on from bee1be8599,
this brings greater parity between theme settings and site settings.

This commit adds a new `disallowed_groups` field to theme and component
settings, which allows theme authors to specify groups that cannot be
selected
by admins for a particular list type or object group type setting.

This is useful for cases where the group-based setting isn't usable
for a group like e.g. anonymous_users, no point allowing them for
a setting that requires a user to be logged in to take effect.

c.f.
https://meta.discourse.org/t/granular-group-based-permissions-for-anonymous-and-logged-in-users/402273/18?u=martin

### List groups

With `disallowed_groups: "4|5"` specified (`logged_in_users` &
`anonymous_users`)

<img width="734" height="450" alt="image"
src="https://github.com/user-attachments/assets/ed8d7338-65fa-44e6-8dce-7073a87076e6"
/>

### Object groups

With `disallowed_groups: "4|5"` specified (`logged_in_users` &
`anonymous_users`)

<img width="696" height="725" alt="image"
src="https://github.com/user-attachments/assets/1a9103d4-cf44-4038-9d62-3838cb779b49"
/>
2026-07-20 09:28:52 +10:00
Martin Brennan 4c7a4f637f UX: Minor chat browse improvements (#41793)
* Sort channels by name
* Show channel emoji next to channel name if it exists
* Add dropdown filter to filter by channel membership
  status (joined/not joined)

<img width="1102" height="458" alt="image"
src="https://github.com/user-attachments/assets/309490fc-abad-48a3-95e0-a037f7ea2f73"
/>
2026-07-20 09:13:19 +10:00
Martin Brennan bee1be8599 DEV: Add resolve_group_membership for theme object settings of group type (#41756)
Followup 7e77ce4bd3

We need to automatically resolve group membership into a boolean
for theme object type settings which are of the group type, similar
to what we did in the original commit above for group list type
settings.

This behaves in the same way -- for an object setting schema like this:

```
menu_sections:
  type: objects
  default:
    - name: section 1
      groups:
        - 1
        - 3
  schema:
    name: menu section
    properties:
      name:
        type: string
      groups:
        type: groups
        resolve_group_membership: true
```

We replace `groups` with a boolean `user_in_groups` (groups is just
the property name, it could be foo_bar etc.) and then you can
do this on the client:

```
for (const section of settings.menu_sections) {
  if (section.user_in_groups) {
    // User is in at least one selected group for this section.
  }
}
```

Rather than inspecting the `currentUser.groups`, which only includes
visible groups, not all groups the user is a member of. This allows
for more accurate permission checks for theme settings that are group
based.

Also c.f.
https://meta.discourse.org/t/granular-group-based-permissions-for-anonymous-and-logged-in-users/402273/18?u=martin
2026-07-17 10:19:29 +10:00
Martin Brennan ec9adc7620 UX: Minor improvements for admin category management (#41754)
Followup d9dfa85e1b

Add action button to create new category, and link
to category discovery route from category badge

<img width="1109" height="658" alt="image"
src="https://github.com/user-attachments/assets/7152e5f3-78ed-4a5d-b9d0-167c4689d5ab"
/>
2026-07-16 13:20:26 +10:00
2ff5b99a4d FEATURE: Embedded Zoom webinar for livestream events (#40973)
This commit adds support for embedding a Zoom webinar
livestream directly within a Discourse Event via a URL in
the location field of the event in the topic.

Admins must supply a Zoom API key and secret in the plugin
settings for this functionality to be available using
these settings:

* livestream_zoom_enabled
* livestream_zoom_sdk_key
* livestream_zoom_sdk_secret

When a topic is opened with a Zoom URL in the event location,
and the Livestream toggle is enabled, the
Zoom library dependencies will be loaded asyncrhonously
and the Zoom webinar will be embedded within the topic page
using the Zoom component view.

On mobile, a fullscreen Zoom client using the Zoom client view
will be used, with a floating Chat button that opens the Discourse
livestream chat in a modal which slides up from the bottom
of the screen in an overlay. This client view is at `/t/:id/:slug/zoom` 

### Webinar event date/time not reached

<img width="880" height="612" alt="image"
src="https://github.com/user-attachments/assets/4b112b59-eed0-4d63-b166-da825d8ebd0b"
/>

### Within 30 minutes before and 10 minutes after the event date/time

<img width="879" height="613" alt="image"
src="https://github.com/user-attachments/assets/373191f8-6245-4b41-b2a8-161c935166be"
/>

### Attendee joined before host, we will retry join every 30s

<img width="735" height="817" alt="image"
src="https://github.com/user-attachments/assets/519afa51-8099-4af0-954f-08da4c7e3297"
/>

### Attendee joined

<img width="733" height="831" alt="image"
src="https://github.com/user-attachments/assets/f633d21f-956e-4bec-a2f8-f7dc8b95d974"
/>

---------

Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: awesomerobot <kris.aubuchon@discourse.org>
2026-07-16 10:58:56 +10:00
Martin Brennan 2062faef44 DEV: Rename AdminFilterControls to DFilterControls (#41719)
The `AdminFilterControls` component isn't really admin specific,
and I want to use it in more places. Renaming to `DFilterControls`.
No backwards compat needed, it's only used in core + core plugins.

Also add `customEmptyState` to `DFilterControls`, which
allows caller to show a custom empty state for `DFilterControls`
in a yield when the array of result items is empty, for cases
where you might want to show e.g. a different message and CTA
when the filter is empty.

E.g. used in Kanban:

<img width="1107" height="439" alt="image"
src="https://github.com/user-attachments/assets/1466e4d9-ac4d-4c59-81e5-6e0e3e259c6f"
/>
2026-07-16 07:53:06 +10:00
Martin Brennan 0be9e90e3d FIX: Argument error for problem checks overriding translation_data (#41724)
ProblemCheck#run was calling translation_data with no args while
persisting tracker/admin notice details. For targeted checks like
`group_email_credentials` and `upcoming_change_stable_opted_out`,
`translation_data` requires the target, so scheduled runs crashed after
call returned a problem. This was causing an error like this in
production:

```
A scheduled admin dashboard problem check (group_email_credentials) errored. : ArgumentError : wrong number of arguments (given 0, expected 1)
/var/www/discourse/app/services/problem_check/group_email_credentials.rb:35:in 'translation_data'
/var/www/discourse/app/models/problem_check.rb:203:in 'ProblemCheck#run'
/var/www/discourse/app/jobs/regular/run_problem_check.rb:23:in 'Jobs::RunProblemCheck#execute'
/var/www/discourse/app/jobs/base.rb:313:in 'block (2 levels) in Jobs::Base#perform'
```

The intended flow after the fix now:

* targets provides the scheduler/tracker key.
* problem(target) is where target-specific translation data is computed.
* Problem#details carries that computed translation data plus
override_data.
* run persists problem.details instead of recalculating
translation_data.

c.f.
https://meta.discourse.org/t/argumenterror-wrong-number-of-arguments-given-0-expected-1-in-upcoming-change-stable-opted-out-problem-check-flooding-logs/407351
2026-07-16 07:52:49 +10:00
Martin BrennanandJordan Vidrine 7ff30d81a3 FEATURE: Support users in DAccessControl and backend (#41358)
Followup 5823e4e3b2,
this commit allows the addition of users along with
groups to access control lists, modifying DAccessControl
to support selecting a user or group from the same
search input.

Shown here is a mix of user & group permissions in the
`DAccessControl` component:

<img width="611" height="664" alt="image"
src="https://github.com/user-attachments/assets/c25e13b0-8885-4ce7-972c-5116f3acb094"
/>

When the search opens, we show the site's groups that
the user can see as preloaded values, showing only the
group name for clarity:

<img width="612" height="389" alt="image"
src="https://github.com/user-attachments/assets/df93a94b-918e-4fed-b045-ac72f02aca41"
/>

When searching a GET request is sent and users are included
in search results.

<img width="608" height="404" alt="image"
src="https://github.com/user-attachments/assets/ff17e6c0-2505-480e-ae25-e6f8309555bc"
/>

---------

Co-authored-by: Jordan Vidrine <jordan@jordanvidrine.com>
2026-07-15 09:18:06 +10:00
Martin Brennanandawesomerobot d9dfa85e1b FEATURE: Category management UI for admins (#41218)
Adds a new `/admin/config/category-management` route which shows
the admin filterable lists of each registered category type, as well as
an "All categories" tab which shows all categories regardless of type,
including badges to indicate type.

This makes it a lot easier for admins to find which of their categories
are configured for each type, and jump to the settings for those
categories:

<img width="2898" height="1488" alt="image"
src="https://github.com/user-attachments/assets/8687a18c-6279-4e66-b3ff-e5694e157430"
/>

<img width="2852" height="1376" alt="image"
src="https://github.com/user-attachments/assets/8e9726b5-c268-400b-9764-451c536695c3"
/>

---------

Co-authored-by: awesomerobot <kris.aubuchon@discourse.org>
2026-07-15 09:17:25 +10:00
Martin Brennan 2e30f331be DEV: Show depends_on site settings for upcoming changes (#41646)
Sometimes for upcoming changes we need to tell the admin about
some settings that must be enabled for the change to take effect.
Currently the only way to do this is with a note in the change
description which is not ideal.

This commit introduces a way to display the depends_on settings
for the upcoming change in the admin upcoming changes page. The
warning only shows if any of the settings are not enabled, if all
dependencies are satisfied we do not show the warning to the admin.

There is a link to each setting from the upcoming changes list
to make it easy for the admin to enable the setting if needed.

<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/724762a9-cc40-45b4-b080-d6ee87dc57ca"
/>
2026-07-14 09:29:47 +10:00
Martin Brennan 8714237760 DEV: Add plugin registry for UC conditional display (#41645)
Upcoming changes conditional display needs to be able to
work for plugin-defined upcoming changes in some cases.
Currently the only way to do this is to extend the
UpcomingChanges::ConditionalDisplay class, which is not ideal.

This commit adds a plugin registry for upcoming changes conditional
display, allowing plugins to register their own conditional display
methods per change.
2026-07-13 11:29:22 +04:00
Martin Brennan b53dc3d9d3 FIX: Do not surface upcoming changes owned by unavailable plugins (#41575)
Upcoming changes can be registered by plugins, but nothing in the
upcoming changes framework knew whether the owning plugin was actually
configurable on a given site.

For unavailble plugins on our hosting, `SiteSettingExtension#setting`
already
forces the plugin's `enabled_site_setting` to false, and `all_settings`
already filters the change out of the Upcoming Changes page.

The notification paths for upcoming changes did not
follow suit...`UpcomingChanges::NotifyPromotion` and
`Jobs::NotifyAdminsOfAvailableUpcomingChanges` both gate only on
`ConditionalDisplay`, so admins were told a change had been enabled by
default for a plugin they cannot use and that is in fact still off.

`UpcomingChanges.enabled?` had the same blind spot for a different
reason, in that it reads the settings provider directly, bypassing the
getter's
configurability guard, so it returned true where the setting itself read
false.

Adds `UpcomingChanges.owning_plugin_configurable?` and short-circuits
both `ConditionalDisplay.should_display?` and `UpcomingChanges.enabled?`
on it. Core changes have no owning plugin and return early, so hot paths
such as `settings_hidden_while_enabled` are unaffected.

The gate keys on `configurable?` alone rather than mirroring the
getter's
narrower `enabled_site_setting == name` condition, a change gating a
sub-feature of an unavailable plugin is equally unavailable.
2026-07-13 10:01:52 +10:00
Martin Brennan 46e6920227 UX: Sort assignment notification menu by topic bump date (#41578)
Currently the /u/username/activity/assigns list sorts by
topic bump date for assignments, whereas the user menu for
assigns list sorts by read/unread and then the created_at
date for the notification.

The latter doesn't make much sense, most of the time when
you are working on assignments the ones you are working on
now which are being bumped somewhat frequently are the
ones you want to see first, not in created_at order.

This commit keeps the unread assignments at the top of the
list, but sorts the rest by topic bump date.
2026-07-13 09:52:22 +10:00
Martin Brennan 2848007d4c DEV: Fix headings for theme setting docs (#41574)
Headings are wrong levels for
https://meta.discourse.org/t/add-settings-to-your-discourse-theme/82557
2026-07-09 11:41:54 +10:00
Martin Brennan 244eb18292 DEV: Docs for theme setting resolve_group_membership (#41537)
Followup 7e77ce4bd3

Add docs for new resolve_group_membership theme setting
behaviour
2026-07-08 17:02:38 +10:00
Martin Brennan 7b0bc78f8c DEV: Bump unified new upcoming change to Beta (#41454) 2026-07-08 09:50:19 +10:00
Martin Brennan 7e77ce4bd3 DEV: Add resolve_group_membership functionality to theme settings (#41360)
Currently, theme settings with `type: list` and `list_type: group`
require client-side permission checks, but `currentUser.groups` only
includes visible groups, not all groups the user belongs to. This makes
permission checks unreliable and can leak hidden group membership.

To address this, this commit adds an opt-in `resolve_group_membership:
true` option that replaces the group ID list with a user_in_SETTING_NAME
boolean resolved server-side via `guardian.in_any_groups?`. The original
group list is removed from the frontend payload to prevent leaking group
IDs.

Since theme settings are cached per-theme (not per-user), the resolution
happens after the cache lookup during per-request serialization in
`ApplicationLayoutPreloader#activated_themes_json`.

Example YAML:

```yaml
copy_button_allowed_groups:
  type: list
  list_type: group
  resolve_group_membership: true
  default: "1|3"
```

Frontend usage:

```javascript
if (!settings.user_in_copy_button_allowed_groups) return;
```
2026-07-08 09:29:21 +10:00
Martin Brennan 943c1ba4cf FIX: Make auto pseudogroups visible to logged on users (#41498)
With the ACL changes that are ongoing, regular users
need to be able to see the pseudogroups in certain situations,
like the ACL group selection dropdown. Pseudogroups are the
everyone, logged_in_users, and anonymous_users groups. They
are currently limited to staff only, but this change will allow
all logged on users to see them as well.

This commit also does some minor fixes related to
granular_anonymous_and_logged_in_groups_permissions
upcoming change. If this is enabled, the everyone group should not
be selectable in Site.groups in the UI, that will just lead to
confusion in the long term especially because this upcoming change
will eventually remove the everyone group.
2026-07-08 09:28:18 +10:00
Martin Brennan ff7a99e851 DEV: Minor fixes to everyone-based permissions for granular_anonymous_and_logged_in_groups_permissions (#41459)
Some minor changes based on findings in
https://meta.discourse.org/t/granular-group-based-permissions-for-anonymous-and-logged-in-users/402273/12?u=martin
, see individual commits for details.

- **DEV: Cleanup hidden_post_visible_groups usage**
- **DEV: Cleanup can_lazy_load_categories?**
- **FIX: Access check for styleguide_allowed_groups in styleguide**
2026-07-07 09:38:56 +10:00
Martin Brennan db7723b686 FIX: Remove Permanent dropdown filter from upcoming changes page (#41455)
Permanent changes show on `/whats-new` but not the upcoming changes
page.
This PR removes the Permanent filter from the dropdown on the upcoming
changes page to avoid confusion.
2026-07-06 12:23:33 +10:00
Martin Brennan 6d87c7b519 DEV: Skip AI caption tests (#41456)
These are failing in the build
2026-07-06 12:23:11 +10:00
Martin Brennan bf386c2fa7 Revert "FEATURE: Bump enable_unified_new upcoming change to beta (#41253)" (#41453)
This reverts commit a0a46c15ae.

Since Beta is now the point where everything is enabled for
self-hosters, we
need to fix specs for this first
2026-07-06 10:02:59 +10:00
Martin Brennan bf3ccea89f FEATURE: Change default upcoming change promotion status to Beta (#41275)
We think this system is tested well enough now to make the default
promotion status for upcoming changes to Beta instead of Stable. This
will mostly affect self-hosters as on our own hosting we set
different promotion statuses based on billing tier.
2026-07-06 09:13:37 +10:00
Martin Brennan 8faf71e3bb FIX: Redirect upcoming change notifications to What's New when permanent (#41385)
When an upcoming change was automatically enabled, the notification
linked admins to the upcoming changes config page. But once a change
reaches permanent status it no longer appears there (that page only
lists experimental through stable) and instead surfaces on the What's
New page. So if the notification was left unread until the change became
permanent, clicking it dropped the admin on an empty filtered list.

This resolves the change's status at click time rather than baking it
into the notification. The permanent change names are now exposed to
staff via the site serializer, and the notification's link checks them:
if every referenced change is now permanent, it sends the admin to
What's New scrolled to that change's card; otherwise it keeps the
existing upcoming changes page where the still non-permanent changes
remain actionable.

To support the scroll, every What's New item now has a stable id anchor,
and the page accepts a scrollTo query param that scrolls to and briefly
highlights the matching card once the feed loads.
2026-07-03 15:28:21 +10:00
Martin Brennan dbfb91149a DEV: Delete fake upcoming change (#41357)
This was just for testing months ago
2026-07-02 11:30:18 +10:00
Martin Brennan 3bd50ee00e FIX: Use display_name for DAccessList (#41311)
When showing groups in the DAccessList component, we need
to fall back to name when full_name is not defined for a group,
so let's call this a single property "display_name", which user
ACLs will use too.

Fixes this:

<img width="616" height="486" alt="image"
src="https://github.com/user-attachments/assets/2c5d21d7-3f33-4666-8244-23b94bac57f7"
/>
2026-07-01 13:16:00 +10:00
Martin Brennan d77cb821d6 FIX: Hide Solved empty_box_on_unsolved on Horizon (#41276)
The Horizon theme doesn't show the empty box on unsolved
topics even if empty_box_on_unsolved is enabled for the
category, so we should hide that setting if the Horizon
theme is default for the site.
2026-07-01 10:09:03 +10:00
Martin Brennan e590d2a2d5 DEV: Add banned_acl concept to AccessControlList (#41254)
Introduces a server-side definition of banned_acl,
similar to mandatory_acl from 5823e4e3b2

This allows AclTarget implementing classes to define
which ACLs cannot be used for certain types. For example:

```
def self.banned_acl
  [{ type: :group, id: Group::AUTO_GROUPS[:anonymous_users], permission: "edit" }]
end
```

This prevents anonymous users group from being able to have the Edit
permission on the target, which is practical because the anonymous
user is not logged in and generally cannot create/edit anything.

This restriction is passed to the DAccessControl component via the Site
serializer, same as mandatory_acl, and is used to prevent the user from
selecting the banned ACLs in the UI.

Then, server-side this rule is enforced in `AccessControlListManager`
service as a policy.

**Before**

<img width="971" height="287" alt="image"
src="https://github.com/user-attachments/assets/af4ace0f-7b11-4005-8344-774d554693f0"
/>


**After**

<img width="914" height="194" alt="image"
src="https://github.com/user-attachments/assets/b756b976-f6e6-4331-b27d-c9a2d13997aa"
/>
2026-06-29 16:22:30 +10:00
Martin Brennan 9bc4254d4c DEV: Additional changes to ACL backend (#41251)
Followup 5823e4e3b2

Add some changes I forgot to commit 🤦
2026-06-29 11:02:51 +10:00
Martin Brennan 5823e4e3b2 FEATURE: Access control list UI and backend (#41009)
Introduces a new way of handling permissions for a target entity
within Discourse, including integration with `Guardian` and `User`
models. For this initial PR, only group-based permissions will work
with this new model, a followup PR will introduce user-based
permissions.

The initial use case for this is securing Kanban Boards in Discourse
Kanban,
see https://github.com/discourse/discourse-kanban/pull/58 for the sister
PR.
In future, we will use this in more places in core, like Chat, Category,
etc.

The new model is called `AccessControlList`, here is the schema:

* `target_id`/`target_type` - Polymorphic columns, can point to any
other model
* `owner` - A string indicating whether the permission is owned by
`core` or a plugin e.g. `discourse-kanban`
* `permission` - A free text field, which can be whatever the target
requires, but defaults are `edit`, `view`, `manage`, and `own`. `own` at
this time is a special permission that should be added to whatever user
creates the ACL at first, but should not be shown in any UI
* `allowed_group_ids` - An array of group IDs which have this permission
* `allowed_user_ids` - An array of group IDs which have this permission

A component, called `DAccessControl`, is also introduced to display
these permissions
and the groups (and soon users) who have them. The component allows a
custom description
label, and the list of permissions can have their text and description
modified,
and can also have permissions removed or added as needed for the target:

<img width="612" height="222" alt="image"
src="https://github.com/user-attachments/assets/88f8d70a-f965-4ef2-9e64-5f8b2bf3fa12"
/>

<img width="598" height="380" alt="image"
src="https://github.com/user-attachments/assets/db90b956-86c2-4ca7-9b63-4c3eb0bad5ef"
/>

Each target entity can define their own `mandatory_acl` array which
is similar to how `mandatory_values` for site settings. For example,
Kanban Boards have a mandatory ACL of the admins group ID having
the `manage` permission for the board, since admins always need to
be able to see + manage boards. This allows us to avoid hardcoding
admin/staff escape hatches in guardian/permissions code.

These will be shown as disabled rows in the `DAccessControl` component:

<img width="577" height="248" alt="image"
src="https://github.com/user-attachments/assets/d9bf1754-0530-4edb-b89f-28887cd613d6"
/>

This commit also introduces a `full_name` calculation for automatic
groups, so we can display a nicer version of the name for things
like `admins`, `staff`, `trust_level_0` in the UI.
2026-06-29 09:44:58 +10:00
Martin Brennan 6b6bfb425b UX: Fix unread + new "topics remaining" links when unified new enabled (#41181)
Fixes the links in the following browse more section at the bottom of
the topic to point to the correct New tab (Topics, Replies) if unified
new is enabled:

> There are UNREAD_LINK and NEW_LINK topics remaining, or browse other
topics in
CATEGORY_LINK

c.f.
https://meta.discourse.org/t/introducing-the-unified-new-view-for-the-topic-list/404728/12?u=martin
2026-06-25 15:55:55 +10:00
Martin Brennan cd98d818dc DEV: Improve JSON schema failure output in API specs (#41182)
The shared JSON endpoint examples used to validate schemas with a
boolean
assertion and print one ad hoc hint to stdout. A failure was hard to act
on:

```
  VALUE AT "/access_control": {"mandatory_acl" => {}}
  POSSIBLE ISSUE W/: /access_control

  expected: true
       got: false
```

That output hid the real validator error, only showed the first failure,
and did not explain whether the response or schema needed to change.

Build a proper RSpec failure message from all JSONSchemer validation
results
instead. Each error now includes the issue, validator error, data path,
schema path, offending value, optional parent/details context, and a
concrete
suggested fix.

For unexpected properties:

```
  JSON schema validation failed with 1 error:

  1. Unexpected property at /access_control
     Error: object property at `/access_control` is a disallowed additional property
     Data path: /access_control
     Schema path: /additionalProperties
     Value:
       {
         "mandatory_acl": {}
       }
     Suggested fix: If this response/request field is intentional, add this entry
     to the parent schema's `properties` object:
       {
         "access_control": {
           "type": "object",
           "additionalProperties": false,
           "properties": {
             "mandatory_acl": {
               "type": "object",
               "additionalProperties": true
             }
           },
           "required": [
             "mandatory_acl"
           ]
         }
       }
       If the field is always present, also add "access_control" to the parent
       schema's `required` array.
```

For missing required properties, the message now groups missing keys and
shows
the response root plus validator details:

```
  Missing required properties default_archetype, notification_types at root
  Error: object at root is missing required properties: ...
  Details:
    {
      "missing_keys": [
        "default_archetype",
        "notification_types"
      ]
    }
  Suggested fix: Add default_archetype, notification_types to the
  response/request, or remove them from `required` at root schema.
```

For type mismatches, the message now points at the mismatched schema
node and
suggests an inferred replacement shape:

```
  Error: value at `/access_control` is not an array
  Data path: /access_control
  Schema path: /properties/access_control
  Value:
    {
      "mandatory_acl": {}
    }
  Suggested fix: Update the payload to match the documented `type`, or replace
  the schema at /properties/access_control with:
    {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "mandatory_acl": {
          "type": "object",
          "additionalProperties": true
        }
      },
      "required": [
        "mandatory_acl"
      ]
    }
```
2026-06-25 15:55:40 +10:00
Martin Brennan 6f75998c0b UX: Add unconfigure category type translation for staff action logs (#41107)
Add the missing `unconfigure_category_type` client translation for staff
action logs
2026-06-23 14:39:32 +10:00
Martin Brennan c0d13c46c6 FIX: Remap category hashtags when slugs change (#41075)
When an admin changes a category's slug or the parent category,
we need to update posts referencing the category with a `#hashtag`
and rebake it with the new hashtag reference.

Now, we enqueue a background job when a category slug or parent
changes to remap matching hashtag references in posts.
2026-06-23 09:28:30 +10:00
Martin Brennan 4d7893aa2b DEV: Add a Discourse admin UI skill (#41040)
- Add a new `discourse-admin-ui` skill distilling
https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780?tl=en
for creating and reviewing consistent Discourse admin interfaces.
- Split guidance into focused references for page shells, content
patterns, filtered settings pages, and plugin admin UIs.
- Include local code anchors so agents can follow current core and
plugin examples without rereading the Meta guide.
2026-06-19 16:19:35 +10:00
Martin Brennan 94cc6d5f5e UX: Update copy of empty checkbox on unsolved topic category setting (#41038)
This checkbox does nothing on Horizon, so make the setting more
vague to indicate themes may do different things with it
2026-06-19 15:03:20 +10:00
Martin Brennan 74a917394c FIX: Do not send notifications for upcoming changes not displayed (#40933)
Some upcoming changes only display based on some condition.
We also need to respect that condition for sending notifications,
since otherwise admins are notified about something they can't
see in the UI.

c.f. https://meta.discourse.org/t/-/405009
2026-06-17 09:10:49 +10:00
Martin Brennan bbd9638e52 FIX: Double Dismiss button for topic list on mobile (#40935)
When floating dismiss topic on mobile UC is enabled,
there should not be a second Dismiss button at the
bottom of the topic list.

**Before**

<img width="420" height="503" alt="image"
src="https://github.com/user-attachments/assets/c07964b1-5e4b-46f2-a089-4bfe196e35ac"
/>


**After**

<img width="420" height="507" alt="image"
src="https://github.com/user-attachments/assets/dbca0d09-41ca-4c4c-837c-cb18252ab938"
/>
2026-06-17 09:10:04 +10:00