Commit Graph
23093 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 404de134f3 Point the API spec and its route checker at the new base
The OpenAPI document keys every operation by its full path, so the spec
described endpoints that no longer exist the moment the server moved.

The openApiSync analyzer cross-checks each registered subrouter against
that document and reconstructs the prefix from a literal of its own, so
spec and analyzer have to move together or every route reports as
missing. Its special case for the v5 root goes away with the root
itself.

The api/v4 directory keeps its name: it is where the spec sources live,
not a URL anyone requests.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 10:58:08 -07:00
zeekayandHanzo Dev c4c2c619ab Serve the HTTP API from /v1/team instead of /api/v4
The product namespace is team, and /api/ is not used as a path prefix
here — API hosts carry the api.* name and paths carry the version. This
moves the one constant every route mounts on, so the public router, the
local router and the websocket endpoint all follow it.

The two 404 catch-alls had the old prefix written out by hand, which
meant the constant was not actually the single source of truth; they now
derive from it, as do the emoji, image-proxy, remote-cluster and
self-ping URLs that were each carrying their own copy of the literal.

The remote-cluster path constants gain a leading slash from the shared
constant, so the two call sites that joined them with an explicit
separator would have produced a double slash and are adjusted.

APIURLSuffixV4 was a bare alias of the value and APIURLSuffixV5 mounted
a subrouter that no route ever registered on, so both are gone rather
than carried forward under names that no longer describe anything.
APIURLSuffixV1 is not ours at all — it addresses the external push
notification service — so it keeps its value and is renamed
PushURLSuffix to stop it reading as a version of this API.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 10:58:01 -07:00
zeekayandHanzo Dev 0eb27d51a9 stop copying an images directory that is gone
Taking the purchase funnel out took src/images/cloud with it, and left the
CopyPlugin entry that names it. webpack does not treat a missing copy source as
an empty set — it fails the build:

  ERROR in unable to locate '/src/webapp/channels/src/images/cloud' glob

which is the whole image build, on a rule for images nothing renders any more.
The four cloud-laptop* files beside it still exist and are still copied; only the
directory entry was stale.

Checked the rest rather than fixing the one that shouted: every other CopyPlugin
source in this config resolves.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 10:45:53 -07:00
zeekayandHanzo Dev d213e4bdb6 drop the browser tests for consoles that are gone
Two Playwright tests drove the upsell pages directly -- one opened Mobile
Security without a licence and asserted the sales pitch, the other opened
Localization and asserted the auto-translation discovery block -- so they now
navigate to screens that do not render. Their page objects go with them,
along with the Edition and License model, which described a console page that
no longer exists and had no test driving it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 00:27:39 -07:00
zeekayandHanzo Dev b165bc03c6 regenerate the English catalogue
Extraction after the removals. 572 strings leave with the screens that
displayed them -- plan names, trial countdowns, purchase copy, seat-overage
warnings, feature-discovery pitches. Two arrive that the rename had already
put in the code but never extracted: the brand mark's label and the Hanzo
sign-in button.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 00:21:55 -07:00
zeekayandHanzo Dev 0e22e2edf5 take the purchase funnel out of the app
Everything that led a user toward buying a licence is gone: the purchase and
downgrade modals and the air-gapped variants that stood in for them, the cloud
usage and limits modals, the trial-benefits, learn-more-trial,
three-days-left-trial and start-trial-form modals, the cloud subscribe/invoice
/fetch-error screens, the cloud effects that polled for a subscription on
login, and the System Console's Billing & Account section, Edition and License
page, and the fourteen feature-discovery routes that showed a sales pitch where
a settings page would be. The licence here is never present and there is
nothing to buy, so none of this could be reached.

Removing a page is easy; the work was untangling what those pages shared with
the rest of the app.

  - components/common/hocs/cloud is not all upsell. withGetCloudSubscription
    and withOpenStartTrialFormModal served only the banner strip, feature
    discovery and the licence page, so they go, but withUseGetUsageDelta is
    how team creation and the team picker learn their usage limits and it
    stays.

  - admin_definition registers the console. The billing section and the
    discovery routes come out by name; the surrounding settings sections and
    their licence gates are untouched. Three sections rendered through
    LicensedSectionContainer, which showed either the settings or an inline
    upsell depending on the licence. Dropping the wrapper leaves them
    rendering as ordinary panels, which is what the licensed branch did
    anyway, with the SKU still shown as a badge.

  - RestrictedIndicator marks a feature the licence does not cover, which is
    honest information, so it survives. What it loses is the click target that
    opened the feature-restricted upsell modal and the eight props that
    existed only to fill that modal in; the lock icon and its tooltip remain.

  - the onboarding checklist's "start a trial" task and the completed screen's
    trial call to action are gone, so the checklist has five tasks and the
    completed screen has one button.

Then the things that were left holding nothing: the screening-in-progress and
purchase-in-progress modals, the payment form and shipping picker, the
cloud-limit menu items and the usage bar and word helpers behind them, the
AD/LDAP upsell banner, the logged-in limit-reached redux flag, twenty
illustration components and the card art. StartTrialFormModal also leaves the
plugin export surface, since the modal it named no longer exists.

Tests that covered deleted components go with them. Where a component
survived, its test now pins what replaced the upsell rather than losing the
coverage: the two admin_definition discovery suites assert that the settings
pages keep their licence gates and that no discovery route stands in front of
them, the burn-on-read and ephemeral-mode suites assert the section renders
its settings with the SKU as a badge, and the onboarding suites assert the
shorter checklist and the single dismissal button.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 00:21:51 -07:00
zeekayandHanzo Dev 38aab33b7d give the About box one pane and one name
The About box had a second, entirely separate pane that rendered when the
licence said Cloud, with its own layout, its own version line and its own
plan-name heading. There is no cloud plan here, so that pane was unreachable
and its only effect was to make every edit to the About box a two-place edit.

The title also read "Hanzo Team Team Edition". The rename to Hanzo Team took
the edition badge off the sign-in header on the grounds that there are no
tiers here, and updated this test to expect a bare "Hanzo Team", but the
component kept appending the licence-tier word to a product name that already
ends in "Team". The heading is now the product name, plus the plan name when a
licence actually carries one; whether the build is enterprise-ready is still
visible in the subtitle and the EE build hash.

The link expectations in the test still asked for utm_source=mattermost, which
stopped being true when we started tagging outbound links with our own source.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 00:19:31 -07:00
zeekayandHanzo Dev b3f0b7346d stop the banner strip from selling licences
Eight of the bars stacked above the channel existed to move an admin toward a
purchase: the cloud trial countdown and its expiry follow-up, the cloud
preview bar, the seat-overage banners in the header and in the invite dialog,
the non-production licence notice, and the two bars whose only job was to open
a start-trial modal on load. None of them can fire here -- they all key off a
cloud subscription or a trial licence, and this server has neither -- so they
were dead weight sitting in the highest-attention strip in the product.

The configuration bar keeps its licence-expiry warnings, since an expiring
licence is a real operational fact an admin needs. What it loses is the
separate trial-expiry bar, which counted down the days and offered "Purchase a
License Now" as the only way out. With commerce gone that bar was a countdown
with no remedy, so an expiring trial now gets the same warning as any other
expiring licence, with the contact-sales link the other branches already use.

The controller no longer needs the cloud subscription it was fetching on mount
solely to decide which of those bars to show.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 00:19:29 -07:00
zeekayandHanzo Dev d8005b50b7 repair three tests the rebrand left behind
The rename to Hanzo Team moved the sign-in OAuth button to the Hanzo brand
mark, so its accessible name is "Hanzo Team logo", not "Gitlab Icon"; the
login test was still asking for the old one. The button is driven by
EnableSignUpWithHanzo/HanzoButtonText, so the test now says so in its name.

The classification markings colour test wrapped userEvent in act(). userEvent
already runs inside act, and the two wrappers race each other on
IS_REACT_ACT_ENVIRONMENT: the inner one clears the flag for the duration of
the async interaction and the outer one finds it false on the way out, so
React warns and the suite's console-error guard fails the test. It only lost
the race when the machine was busy, which is why it passed alone and failed
in a full run. Dropping the redundant act removes the race.

The brand mark import in login and signup sat out of alphabetical order and
had been failing lint since it was added.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 00:19:28 -07:00
zeekayandHanzo Dev 03353f6128 declare the version this image publishes
imgver refused the build: "no version for ghcr.io/hanzoteam/server ... We don't
ship shas." Nothing at the root carried one — a Go + npm tree has no single
manifest, so package.json/Cargo.toml/pyproject.toml all miss and VERSION is the
file that answers for a repo shaped like this one.

0.1.0, and its own series rather than upstream's 11.11.0. Hanzo Team is not a
build of Mattermost with a different logo — the enterprise tree is gone, the seat
cap is gone, sign-in is Hanzo IAM — so a number that tracked upstream would claim
a lineage the artifact does not have. imgver publishes exactly what is declared
while it leads the registry, and derives the next patch once it does not, so this
is the last time the number is typed.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 23:51:23 -07:00
zeekayandHanzo Dev 61fefb7b8d track go.work, and build the shipped binary as a shipped binary
The gate failed on `[build failed]` for symbols that are in this tree. Upstream
publishes server/public and pins v0.4.0 of it; we changed server/public, so
anything that reads the pin compiles against a release with no HanzoSettings, no
ServiceHanzo, no UserAuthServiceHanzo. The workspace is what makes ./public mean
this tree, and it was ignored — so `go build`, `go test`, the gate and the image
each had to arrange it privately, and the one that did not simply failed.

Tracking it states the fact once. The image build no longer arranges anything.

Also:

  -tags production. Without it server/public/model selects the DEV service
  environment through a build constraint, and a shipped binary points at dev
  telemetry and licensing while looking entirely normal.

  The runtime tree — data, logs, config, plugins, client/plugins — is made in a
  stage that has a shell, because distroless does not and the server has nowhere
  to write without them.

  The document-extraction binaries are dropped rather than carried. pdftotext
  wants GLIBC_2.38 and libstdc++ on a 2.36 base that has no libstdc++ at all;
  wvText is a shell script and there is no shell. The library COPYs beside them
  read /usr/lib while Ubuntu keeps those under /usr/lib/x86_64-linux-gnu, and a
  --from glob matching nothing is a silent no-op — so upstream's image ships five
  binaries with no libraries and reports success. Restoring extraction needs a
  base that can run it; that is its own decision.

  .dockerignore, because .git alone is 1.2 GiB of context and node_modules is a
  working tree's results standing in for the build's.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 23:48:07 -07:00
zeekayandHanzo Dev dc07a05eb7 build the image from this tree, and publish it from the forge
Upstream's server/build/Dockerfile downloads a released tarball. A fork cannot
use it: the thing we changed is the source. This builds the web app and the
server here and assembles the runtime layout the binary looks for — /hanzo,
found by CWD rather than by a hardcoded path, so the name is ours.

Three things the tarball made invisible and a source build has to state:

  the platform workspaces are built by npm's postinstall, so the install needs
  the tree, not the manifests;

  ./public must resolve to this tree — server/go.mod pins the published v0.4.0,
  which has none of the Hanzo model;

  distroless has no shell, so data/, logs/, config/, plugins/ and client/plugins
  are laid out in the stage that still has one, or the server has nowhere to
  write and does not start.

hanzo.yml declares the one image; the workflow is the ~7-line caller of
hanzoai/ci. The gate is the IAM claim mapping and the tenant boundary — the two
behaviours a compile cannot catch and that put a signing-in user in the wrong
org when they are wrong.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 23:42:17 -07:00
zeekayandHanzo Dev 1062c591c2 send our own tracking source, not upstream's
The remaining utm_source=mattermost assertions follow the hook that
already stopped emitting it.

The 17 tests still failing all exercise the cloud, billing and
feature-discovery components -- the surface that sells a licence. They
assert Contact Sales CTAs, SKU labels and invoice templates, none of
which this build can reach: the licence is always nil. Deleting those
components is the honest fix and it is a real piece of work; the cloud
HOCs they share are wired into team creation and the invite flow, so a
partial cut leaves the tree broken. Recorded, not papered over.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 23:07:46 -07:00
zeekayandHanzo Dev 6e62296b89 keep the desktop token a token, and stop tagging other people's links
Renaming the product caught two strings that are protocol, not prose:

  - isDesktopApp matched the User-Agent the desktop client sends, so
    "Hanzo Team" with a space broke detection outright. The token is
    HanzoTeam -- one word, because that is what a UA token is -- and the
    label stays readable. Every fixture spells it the same way.
  - Outbound links carried utm_source=mattermost and were tagged only on
    mattermost.com. They now carry our source, on our domains; a link to
    a site we do not own leaves without our parameters attached.

The rest is test truth catching up: the default theme is hanzo, a job
title in a fixture gained a word so its suffix suggestions gained an
entry, and 373 snapshots re-recorded. Those were regenerated under
TZ=Etc/UTC, which the project's test script sets and I had initially
missed -- the first pass baked local time into them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 22:56:01 -07:00
zeekayandHanzo Dev d92ed76c01 retire the edition badge from the app header
The product menu chose between two brandings on licence state, and the
free one existed to print TEAM EDITION or ENTRY EDITION next to the
wordmark. With no tiers there is nothing to label, so there is one
branding again.

The two tests that asserted the badge are replaced by one that pins the
new invariant: the branding is the same whatever the licence says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 22:36:39 -07:00
zeekayandHanzo Dev 05e7bfa24b call the product Hanzo Team, and paint it in one ink
Renames the product across user-facing text -- 1475 lines, i18n and the
defaultMessage strings that must match it -- while leaving the names that
are code rather than prose: upstream copyright, Go import paths, the
@mattermost npm scope, i18n keys, Go template fields, and legacy theme
identifiers like AuthorType.Mattermost and the 'Mattermost' key that maps
an old theme to a new one. The rule is a word boundary on identifier
characters; MattermostEntry is a symbol, "Mattermost" in a sentence is
the product.

The theme goes monochrome. Hanzo and Hanzo Dark are the two built-ins and
Hanzo is the default, with the CSS custom properties that carry brand
colour outside the theme object -- sidebar, buttons, links, titles --
moved to black. Status colours stay: online, away, do-not-disturb and
error encode state, not brand, and flattening them to grey would cost
meaning to buy consistency.

The sign-in header drops the TEAM EDITION badge. It advertised which
licence tier you were on, and there are no tiers here. Booting without a
licence is now logged at debug rather than warned about, because it is
this server's normal condition rather than a fault.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 22:28:09 -07:00
zeekayandHanzo Dev 9ce234d312 ship pointed at Hanzo AI, and stop the survey plugin phoning home
Agents arrives configured against api.hanzo.ai with the model "best" --
the gateway's own routing alias, so the default does not go stale as
models come and go. The API key is deliberately absent; it is delivered
from KMS at deploy.

NPS sends satisfaction surveys to Mattermost. It was enabled whenever
diagnostics were, and diagnostics now default off, but the coupling read
as an accident waiting to be re-enabled. It is off on its own terms.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 22:12:25 -07:00
zeekayandHanzo Dev 021d8848d6 show one sign-in button, and let it say Hanzo
The login and signup screens read the renamed slot, route to
/oauth/hanzo, and draw the Hanzo mark instead of the GitLab glyph --
BrandMark rather than a second copy of the same five paths.

The GitLab entry in the admin console was a feature-discovery panel: a
teaser that exists to sell a licence, not a setting anyone can change.
It goes with the rest of the upsell.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 22:10:00 -07:00
zeekayandHanzo Dev 5979de1824 sign in through Hanzo IAM, and place people by their org
The generic OAuth2 slot upstream calls GitLab is not GitLab-shaped: it is
scope, auth endpoint, token endpoint, user endpoint. Here it is Hanzo
IAM, so it is named that and defaults to hanzo.id's advertised endpoints.
An operator supplies a client id and secret; nothing else.

Three things had to move for that to actually work:

  - The client config only emitted the SSO props under a license, so the
    sign-in button never rendered without one. Hanzo IAM is this
    server's identity provider, not a licensed add-on, so its props are
    unconditional.
  - getSSOProvider rerouted any openid-scoped service to an OpenID
    provider this build does not register, and IAM's scope is
    "openid email profile". One service, one provider.
  - The provider reads sub as the identity. Email and username move;
    sub does not.

Tenancy rides on the owner claim, which IAM treats as authoritative:
each org gets an invite-only team named for it, created on the first
sign-in from that org and joined on every one after. The claim is read
fresh each time, so someone moved between orgs in IAM lands in the right
team on their next login. A profile field cannot influence this -- only
the claim can.

Tests cover the claim mapping and the tenant boundary, including that a
user from one org does not land in another's team. The GitLab provider's
tests are gone with the provider; these replace them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 22:04:59 -07:00
zeekayandHanzo Dev 00b9c1ebf9 draw the product in the Hanzo mark, monochrome
Logo, wordmark, email art and the whole favicon set are regenerated from
the five-block Hanzo glyph in @hanzo/brand, set in Geist. Nothing carries
a second colour, so the tab badges had to change shape rather than hue:
a mention is a filled disc, an unread is a ring, and the mark shrinks to
leave the corner clear -- an overlapping badge reads as a smudge at 16px.

logo.svg and favicon.svg follow prefers-color-scheme, and the inline mark
takes currentColor, so one asset serves both themes. root.html now offers
the vector favicon first; the PNGs stay as fallback.

MattermostLogo becomes BrandMark: it draws the mark, so it is named for
what it draws.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 21:08:52 -07:00
zeekayandHanzo Dev 53c27715ff remove the licensing commerce: seat cap, cloud billing, trial requests
An unlicensed server hard-capped at 250 users (maxUsersHardLimit, no
grace period) and blocked user creation and reactivation at that line.
That is a commercial seat limit, not a safety limit, and a standalone
deploy cannot carry it. GetServerLimits and its two enforcement sites
in user.go are gone.

Also gone: the cloud subsystem that talked to customers.mattermost.com
and portal.internal.prod.cloud.mattermost.com -- billing, subscriptions,
invoices, trial license requests, daily seat reporting, IP filtering,
and the post/file history caps that expressed plan tiers.

Two plugin API methods stay as stubs. GetCloudLimits and
RequestTrialLicense are declared in plugin.API, and plugin binaries are
compiled against those signatures; removing them from the interface
would break a plugin that references either. They report no limits and
no trial.

The post and file accessibility filters stay. They are generic
"hide everything before time T" machinery that reads a stored timestamp,
and with nothing writing that timestamp they return at their first
check. Rewriting 25 read paths to delete an unreachable branch buys
nothing.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 21:02:40 -07:00
zeekayandHanzo Dev 88633884c0 drop upstream project infrastructure
The docs site (209M), Cypress suite (34M), GitHub Actions workflows and
the Cursor cloud-agent environment all describe Mattermost's own
project: their doc domain, their runners, their enterprise repo as a
Cursor multi-repo dependency. None of it describes this fork.

Playwright stays -- it is the e2e suite we run. Builds move to arcd.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 20:48:40 -07:00
zeekayandHanzo Dev 109a1c90f7 remove the source-available enterprise tree
server/enterprise/ ships under the Mattermost Source Available License,
which permits production use only under an Enterprise subscription. A
fork that runs standalone cannot carry it, so it goes -- along with the
build plumbing that existed to find it: the enterprise/sourceavailable
build tags, BUILD_ENTERPRISE_READY and its dead ifeq branches, the
EE/Elasticsearch/OpenSearch test targets, and the ldap mock generator.

What is left is the AGPL core, which upstream already calls the team
build (BUILD_TYPE_NAME = team). Prometheus metrics, Elasticsearch and
compliance export went with the tree; database search covers search.

Prepackaged plugins narrow to the four we actually serve: agents, calls,
playbooks, boards.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 20:48:07 -07:00
cefbe99402 MM-70072: Update team admin assignment during team join (#37922)
* MM-70072: Update team admin assignment during team join

* Assert SchemeUser in team rejoin test case

* MM-70072: Fix team admin assignment in bulk import path

* Preserve computed admin status through scheme role sync in bulk import

---------

Co-authored-by: Bill Gardner <[email protected]>
2026-08-13 14:22:06 -04:00
60af6c5c86 Add weekly recurring scheduled posts (#37746)
* Add weekly recurring scheduled posts.

Extend scheduled posts so users can schedule weekly repeats and keep the series healthy across sends, reschedules, and UI updates instead of falling back to one-shot behavior.

Made-with: Cursor

* Add Playwright coverage for recurring scheduled posts.

Cover weekly recurring scheduled messages in the scheduled-messages spec so the recurring UI and reschedule flow stay protected without adding a separate test surface.

Made-with: Cursor

* Fix recurring scheduled post CI failures.

Resolve the initial lint and formatting issues and renumber the new scheduled-post migration so it no longer collides with master during Postgres-backed test setup.

Made-with: Cursor

* Sync recurring scheduled post translation files.

Regenerate the affected English translation catalogs so the recurring scheduled post strings match the source extraction order expected by CI.

Made-with: Cursor

* Fix recurring scheduled post review follow-ups.

Preserve overdue cleanup behavior during weekly catch-up, defer delete websocket events until deletion succeeds, and address the remaining migration and UI review nits.

Made-with: Cursor

* Address recurring scheduled post review feedback

Made-with: Cursor

* Fix recurring scheduled post CI checks

Made-with: Cursor

* Make pending scheduled post keyset cursor index-scannable

EXPLAIN ANALYZE on a 5M-row ScheduledPosts table showed the pure OR-form
cursor forced Postgres to scan idx_scheduledposts_pending_scheduled_at_id
from the top on every page (~383ms/page, ~2M rows filtered). Keeping the
ScheduledAt <= beforeTime bound outside the tie-break restores the index
boundary (~0.12ms/page). Adds storetest coverage for cursor pagination.

Co-authored-by: nick.misasi <[email protected]>

* Address code quality review findings for recurring scheduled posts

Server:
- Replace silent-fallback AdvanceWeeklyScheduledNextOccurrence with
  error-returning ScheduledPost.ComputeNextScheduledAt; a recurring post
  whose timezone fails to load is now routed through the failed-post path
  instead of being reposted every job run or silently deleted
- Add ScheduledPost.IsRecurring and partition batches in
  processScheduledPostBatch; advance/delete now run independently so a
  store failure in one path can't cause reposts in the other
- Drop dead generality in UpdateRecurringScheduledPosts (only ScheduledAt
  varies per row; ErrorCode/ProcessedAt are constants)
- Simplify redundant repeat-type condition in GetPendingScheduledPosts

Webapp:
- Add shared isRecurringScheduledPost helper, replacing six scattered
  repeat_type === 'weekly' literals
- Recurrence timezone is now simply the scheduler's current timezone;
  removes initialRepeatTimezone/effectiveTimezone plumbing and the
  modal-label/picker timezone mismatch
- Single enforcement point for hiding send-now on recurring posts
- Use canonical getTeamIdByChannelId at both SCHEDULED_POST_UPDATED
  dispatch sites; rewrite errorsByTeamId update case as remove-then-add
  and add reducer tests

Co-authored-by: nick.misasi <[email protected]>

* Simplify recurring scheduled post code per review

- End a recurring series when its channel no longer exists instead of
  advancing it forever (matches the one-shot channel-not-found handling)
- Collapse the errorsByTeamId SCHEDULED_POST_UPDATED case into the
  identical SINGLE_SCHEDULED_POST_RECEIVED case (a scheduled post's team
  can't change) and combine duplicate byId cases; preserves state
  references on no-op updates
- Drop no-op timezone conversions in ComputeNextScheduledAt
- Remove redundant checkbox aria-label (label htmlFor already names it)
- Schedule new job tests in the past instead of sleeping a real second
- Remove redundant test assignment and e2e positional boolean

Co-authored-by: nick.misasi <[email protected]>

* Remove slop from recurring scheduled post changes

- Drop the business-rule CHECK constraint from the recurrence migration;
  no other migration enforces model-layer validation in the database,
  and BaseIsValid plus the job's failure handling already own it
- Fold the standalone repeat-validation test file into the existing
  TestScheduledPostBaseIsValid, matching its conventions
- Revert unrelated benchmark modernization in utils_test.go
- Drop an unneeded cast, unused fixture fields, and naming/assertion
  inconsistencies in tests

Co-authored-by: nick.misasi <[email protected]>

* Regenerate ScheduledPostStore mock with mockery ordering

Co-authored-by: nick.misasi <[email protected]>

* Address CodeRabbit review feedback

- Reject the host-dependent 'Local' value for RepeatTimezone; recurring
  schedules need a fixed zone (UTC or IANA name)
- Hide reschedule for deactivated DMs, matching send-now eligibility

Co-authored-by: nick.misasi <[email protected]>

* Resolve scheduled post team bucket from existing state when channel is unloaded

An admin can reschedule before fetchMissingChannels resolves, in which case
deriving the team from the channel returns undefined and the update was
misfiled under directChannels. getScheduledPostTeamId falls back to the
byTeamId bucket that already holds the post.

Co-authored-by: nick.misasi <[email protected]>

* Retrigger CI after transient enterprise npm network failure

Co-authored-by: nick.misasi <[email protected]>

* Clear hover/focus before asserting scheduled post header details in e2e

The drafts panel hides its timestamp/tag info section while hovered or
focus-within; after the reschedule modal closes, focus returns to the row
and the 'Repeats weekly' tag assertion saw a hidden element.

Co-authored-by: nick.misasi <[email protected]>

* Move recurrence columns into baseColumns and dispatch ComputeNextScheduledAt on repeat type

Co-authored-by: nick.misasi <[email protected]>

* Preserve recurrence when scheduled post updates omit repeat fields

Co-authored-by: nick.misasi <[email protected]>

* Disallow file attachments on recurring scheduled posts

Co-authored-by: nick.misasi <[email protected]>

* Stop pinning the channel indicator for recurring-only scheduled posts

Co-authored-by: nick.misasi <[email protected]>

* Re-home nullable-Type comment onto baseColumns

Co-authored-by: nick.misasi <[email protected]>

* Gate recurring scheduled posts behind a default-off feature flag

Co-authored-by: nick.misasi <[email protected]>

* Move presence-preservation rationale to the copy site

Co-authored-by: nick.misasi <[email protected]>

* Extract draftHasAttachments helper and require allowRecurring at single-caller layers

Co-authored-by: nick.misasi <[email protected]>

* Return null from the indicator selector when nothing should show

Co-authored-by: nick.misasi <[email protected]>

* Gate only recurrence transitions and preserve existing series in the modal

Co-authored-by: nick.misasi <[email protected]>

* Avoid err shadowing in scheduled post update handler

Co-authored-by: nick.misasi <[email protected]>

* Disable the repeat weekly checkbox with a tooltip when the message has attachments

Co-authored-by: nick.misasi <[email protected]>

* Return an explicit disposition from postScheduledPost

The batch loop inferred 'channel permanently gone' from the one return
path with a nil error and an error code set - an invariant a future
change could silently break, deleting recurring series by accident.
postScheduledPost now returns posted/failed/unsendable explicitly, the
switch refuses to delete on an unhandled disposition, and a test pins
that both recurring and one-shot posts in a nonexistent channel are
permanently deleted.

Co-authored-by: nick.misasi <[email protected]>

* Fix Repeat weekly attachments tooltip centering on the modal row (#37927)

Shrink-wrap the repeat checkbox row so WithTooltip anchors to the
control/label instead of the full modal body width.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 07:27:23 -04:00
ac0705fb68 [MM-69226] Fix Manage Teams role dropdown overflowing outside the modal (#37400)
* Fix Manage Teams dropdown overflowing outside modal for last rows

The role dropdown in the Admin Console Manage Teams modal always opened
downward, so for users with many teams the menu for the last rows was
pushed below the modal and out of view. Compute openUp for rows near the
bottom of the list, mirroring the TeamMembersDropdown behavior.

Co-authored-by: mattermost-code <[email protected]>

* Render Manage Teams role dropdown via floating menu to fix overflow

Migrate ManageTeamsDropdown from the deprecated Menu/MenuWrapper widget to
the modern components/menu (MUI Popover). The old widget positioned the
menu relative to the modal content, so for users with many teams the role
dropdown on the last rows was pushed below the modal and off-screen. The
new menu renders in a portal anchored to the trigger and opens upward for
rows near the bottom of the list, mirroring TeamMembersDropdown.

Co-authored-by: mattermost-code <[email protected]>

* Add openUp helper coverage and action tests for ManageTeamsDropdown

Extract the open-up positioning rule into a testable shouldOpenUp helper
and cover its boundaries, plus the demote and error paths, so the fix for
the dropdown overflow is exercised directly.

Co-authored-by: mattermost-code <[email protected]>

* Satisfy lint for table-driven test comment

Co-authored-by: mattermost-code <[email protected]>

* Gate onMemberChange assertions behind waitFor in dropdown tests

Move onMemberChange expectations inside waitFor so they wait for the
async updateTeamMemberSchemeRoles promise to resolve before asserting.

Co-authored-by: mattermost-code <[email protected]>

* Fix Manage Teams e2e to target MUI menu button and items

The Manage Teams role dropdown was migrated from the deprecated
components/widgets/menu (div.MenuWrapper) to the modern components/menu
(MUI Popover). The Playwright spec still clicked div.MenuWrapper and
looked for menu items inside the team row, but the new menu renders in a
portal. Target the accessible role button and role menuitem elements
instead.

---------

Co-authored-by: mattermost-code <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 09:24:45 +02:00
a117d81f54 [MM-70226] Migrate role GetByName to request context (#37634)
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 06:33:28 +00:00
35488124e1 [MM-69911] Include PAT token ID in server and audit logs for request traceability (#37910)
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 08:00:48 +02:00
Harshil SharmaandGitHub 8e383dd9d0 Added DM GM restriction for flagging a post (#37841)
* Added DM GM restriction for flagging a post

* Added DM/GM check in other content reviewer paths as well

* Fixed an order of operation
2026-08-13 08:05:24 +05:30
Jesse HallamandGitHub 932443f0ef Log an error instead of refusing to start on unsupported Postgres, Elasticsearch, and OpenSearch versions (#37929) 2026-08-12 16:55:41 -04:00
017e261ed4 MM-70240: Adjust post and thread payload sanitization (#37920)
* MM-70216: Adjust post and thread payload sanitization

* MM-70216: Preserve MM blocks actions in thread test fixture

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-12 15:07:08 -04:00
Devin BinnieandGitHub 5b979e5c72 [MM-70189] Add operators for CIDR and version checks to the simple policy editor (#37918) 2026-08-12 16:25:48 +00:00
df7f64a4bd [MM-64357] Fix ABAC policy editor unable to switch back to Simple Mode when a value contains an apostrophe (#37819)
* [MM-64357] Fix ABAC simple-expression detection for apostrophe values

The isSimpleCondition/isMultiselectOrGroup helpers used a quoted-string
regex fragment (['"][^'"]*['"]) that rejected any quote character inside
a value. A valid value like "Matt's Department" therefore failed the
simple-expression check, disabling "Switch to Simple Mode" and trapping
the policy editor in advanced mode.

Replace the fragment with a proper CEL string-literal pattern that allows
apostrophes inside double-quoted strings (and double quotes inside
single-quoted strings), plus escaped quotes, matching what the CEL parser
accepts and what celStringLiteral emits.

Co-authored-by: mattermost-code <[email protected]>

* [MM-64357] Add tests for quote characters in ABAC simple-expression detection

Cover the apostrophe regression: values like "Matt's Department" must be
classified as simple expressions across equality, string operators, in-lists,
scalar-in, native email, single-quoted values, and multiselect OR groups. A
round-trip test ties rowToCEL/celStringLiteral output to isSimpleExpression,
and a negative case guards against accepting unterminated quoted values.

Co-authored-by: mattermost-code <[email protected]>

* [MM-64357] Strengthen quote-handling tests per review

Add bug-catching round-trip for "has any of" (multiselect OR group), plus
ranked-operator, session-namespace, and unescaped-embedded-quote cases.
Clarify that the in-list matcher and single-quoted case are intentionally
detection-only, and reword the celStringLiteral characterization comment.

Co-authored-by: mattermost-code <[email protected]>

* [MM-64357] Tighten CEL list pattern in simple-expression detection

Co-authored-by: mattermost-code <[email protected]>

* Add editor-surface and e2e tests for apostrophe values (MM-64357)

Address review feedback requesting the coverage gaps Linda/Matty flagged:
- Membership Policy surface (policy_details): rendered mode-toggle state
  (Switch to Simple Mode enabled) for apostrophe equality and multiselect
  OR-group values, plus a complex-expression negative control.
- Permission Policy surface (permission_policy_details): apostrophe
  equality/in-list/OR-group open in Simple mode and keep the toggle enabled,
  covering the shared isSimpleExpression path on both surfaces.
- Playwright e2e: apostrophe 'is' value round-trips through save/reopen and
  the Switch to Simple Mode toggle stays enabled (button state + persistence).

* Fix apostrophe values e2e test cleanup to be independent of list rendering

- Add fallback policy lookup by name in finally block
- Ensures cleanup happens even when navigation/assertions fail after save
- Addresses CodeRabbit feedback on test stability

Co-authored-by: Maria A Nunez <[email protected]>

---------

Co-authored-by: mattermost-code <[email protected]>
Co-authored-by: Maria A Nunez <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-12 10:49:26 -04:00
Christopher PoileandGitHub 89110328f1 [MM-70106] Prevent search startup bulk processor leaks (#37873)
* the main fix + unit tests

* add actual analysis-icu-less docker containers in CI to lock it down

* fix AppError test assertions

* fix CI search startup tests

* fix bulk processor flusher shutdown

* fix bulk shutdown test assertion

* Revert "add actual analysis-icu-less docker containers in CI to lock it down"

This reverts commit dfa9aacb08235047414096084543ffca980c7e65.

* simplify analysis-icu startup guidance

* fix OpenSearch bulk flusher shutdown

* linting fix

* query nodes directly for plugins instead of trying and catching err

* remove unneeded comments
2026-08-12 10:40:24 -04:00
690f883ac8 Fix link preview image layout shift by using SizeAwareImage (#37357)
* Fix link preview image layout shift by reserving space with SizeAwareImage

Open graph link previews rendered a plain <img> with no upfront dimensions.
When metadata reported large images (e.g. 2400×1256), the preview started at
zero height and expanded after load, pushing content below and causing scroll
pop in the channel and permalink views.

Use SizeAwareImage (same approach as post images and message attachments) so
known dimensions reserve layout before the image loads. Add
getScaledImageDimensions() to scale metadata down to the preview's CSS limits
(392×240 for large images, 80×80 for thumbnails) so placeholders match the
final rendered size rather than SizeAwareImage's default 350px cap.

Centralize layout constants in constants.ts and sass/utils/_variables.scss
with cross-reference comments, and wire post_attachment_opengraph.scss to
those variables to keep TS layout math and CSS in sync.

Also hide copy/download utility buttons on link preview images via
hideUtilities={true}.

* Change the opengraph dimention variables from SCSS to CSS

* Lint and update snapshot

* Add additional E2E test

This test passes on master in Chrome but not in Firefox.

* Update URL in opengraph-huge.html

---------

Co-authored-by: Harrison Healey <[email protected]>
2026-08-12 10:39:59 -04:00
a50f2ac4fd [MM-69557] Fix post actions menu not closing on outside click in mobile view (#37394)
* [MM-69557] Fix post actions menu not closing on outside click in mobile view

In the narrow/mobile-responsive layout the post actions ("…") menu renders
as a full-height GenericModal. React-bootstrap only dismisses the modal when
its outer container is clicked directly, but the full-height dialog element
intercepted every click in the dimmed area around the menu, so clicking
outside never closed it.

Let clicks on the empty dialog area fall through to the modal container via
pointer-events (scoped to .menuModal) so clicking outside dismisses the menu,
matching standard popup behavior, while keeping the menu itself interactive.

Co-authored-by: mattermost-code <[email protected]>

* [MM-69557] Add E2E test for mobile post actions menu outside-click dismissal

Adds a Playwright spec covering the mobile/narrow layout where the post actions
("…") menu renders as a full-screen modal. Verifies clicking the dimmed area
outside the menu (both above and below the menu list) dismisses it, and that
selecting a menu item still works. The outside-click test fails without the
pointer-events fix, so it is coupled to the real behavior.

Co-authored-by: mattermost-code <[email protected]>

* [MM-69557] Use type-only import for PlaywrightExtended in mobile dot menu spec

Satisfies the @typescript-eslint/consistent-type-imports lint rule.

Co-authored-by: mattermost-code <[email protected]>

* [MM-69557] Don't treat pointer-events: none as non-focusable in focus trap

The mobile menu fix sets pointer-events: none on .menuModal (restoring
auto on .modal-content). useFocusTrap's isElementVisible() walked the
ancestor chain and rejected any element under a pointer-events: none
ancestor, which broke keyboard focus trapping inside the modal.

pointer-events only affects mouse/touch hit-testing, not keyboard
focusability, and can be re-enabled on descendants, so remove it from
the visibility check. Adds a regression test.

* Address PR feedback: make focus-trap test spy cleanup exception-safe

---------

Co-authored-by: mattermost-code <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-12 08:58:55 -04:00
747e429378 MM-70151/MM-70152: fix slash commands in the WYSIWYG composer (#37880)
Co-authored-by: Nevyana Angelova <[email protected]>
2026-08-12 13:18:06 +03:00
bd753cd18f Remove unused context import left behind by UserStore.Get migration (#37921)
PR #37646 (Migrate UserStore Get to request context) migrated
context.Background() call sites to request.CTX but left the now-dead
"context" import in place, assuming other usages in the same files
would keep it alive. A concurrently merged sibling PR (#37637,
GetAllProfilesInChannel migration) removed those other usages first,
so by the time #37646 landed on top of the moved master tip the
import had no remaining references, breaking go vet/build on master.

Affected: server/channels/store/sqlstore/user_store.go,
server/channels/store/storetest/user_store.go,
server/channels/store/storetest/mocks/UserStore.go,
server/channels/store/localcachelayer/user_layer.go(_test.go),
server/channels/app/post_persistent_notification.go

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-12 10:07:43 +00:00
f8a5d1c3c5 [MM-70222] Migrate UserStore Get to request context (#37646)
* Migrate UserStore Get to request context

Co-authored-by: Ben Schumacher <[email protected]>

* Clean up context imports after UserStore Get migration

Co-authored-by: Ben Schumacher <[email protected]>

* Fix UserStore Get test context fallout

Co-authored-by: Ben Schumacher <[email protected]>

* Update app interfaces for request-aware user get

Co-authored-by: Ben Schumacher <[email protected]>

* Update UserStore Get test fakes

Co-authored-by: Ben Schumacher <[email protected]>

* Thread request context through user get callers

Co-authored-by: Ben Schumacher <[email protected]>

* Use email service logger for batching context

Co-authored-by: Ben Schumacher <[email protected]>

* Pass request context to trial license lookup

Co-authored-by: Ben Schumacher <[email protected]>

* Pass request context to buildShownByList

Co-authored-by: Ben Schumacher <[email protected]>

* Pass request context to buildFavoritedByList

Co-authored-by: Ben Schumacher <[email protected]>

* Add request context to PromoteGuestToUser

Co-authored-by: Ben Schumacher <[email protected]>

* Pass request context to trial and demote flows

Co-authored-by: Ben Schumacher <[email protected]>

* Use request context in AddUserToTeam lookup

Co-authored-by: Ben Schumacher <[email protected]>

* Use plugin request context for trial license

Co-authored-by: Ben Schumacher <[email protected]>

* Use request contexts throughout team user lookups

Co-authored-by: Ben Schumacher <[email protected]>

* Document future GetUser request context migration

Co-authored-by: Ben Schumacher <[email protected]>

---------

Co-authored-by: Ben Schumacher <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-12 09:33:46 +02:00
86f6d83e03 [MM-70225] Migrate Store.GetDiagnostics to request.CTX (#37635)
* Migrate store diagnostics to request context

Co-authored-by: Ben Schumacher <[email protected]>

* Keep store context rule only in server AGENTS

Co-authored-by: Ben Schumacher <[email protected]>

---------

Co-authored-by: Ben Schumacher <[email protected]>
Co-authored-by: Mattermost Build <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-12 09:03:09 +02:00
24c3d5c884 [MM-70223] Migrate GetAllProfilesInChannel to request context (#37637)
Co-authored-by: Ben Schumacher <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-12 09:02:22 +02:00
Doug LauderandGitHub f4d1e25012 MM-69886: Refresh Channel Members RHS on websocket add and reconnect (#37584)
* MM-69886: Refresh Channel Members RHS on websocket add and reconnect

The Channel Members RHS (participant list) could omit members in two
general cases:

- On a user_added websocket event, handleUserAddedEvent recorded the
  profile-in-channel entry and loaded the profile but never loaded the
  ChannelMembership. buildProfileList drops any user without a
  membership, so a member added while the channel was open did not
  appear until the list was fully reloaded. It also affected members
  whose profile was already loaded (e.g. after they posted) but who
  still had no membership. Fetch the membership via getChannelMember
  when it is missing.

- On websocket reconnect, reconnect() reloads only the current user's
  own memberships, never the channel roster, and missed user_added /
  user_removed events are not replayed on a fresh (non-resumed)
  connection. Members added or removed while disconnected stayed stale
  until the channel was re-opened. The RHS now reloads the roster when
  the websocket connection id changes.

This extends the reconcile-on-load work from MM-68660 (PR #36964, same
loadProfilesAndReloadChannelMembers path), which pruned removals only
on channel (re)load and did not cover live adds or reconnects.

Found while re-testing MM-67616 (Shared Channels), where synced members
were not reflected in the participant list even though the server-side
sync was correct.

* Add comment to caution about changing remote cluster service ping frequency.
2026-08-11 16:44:25 -04:00
4f16eba1bd [MM-69726] Fix Message actions menu displaying off screen in narrow display (#37396)
* [MM-69726] Fix Message actions menu rendering off screen in narrow view

In mobile/single-column view the post Message actions menu renders as a
full-screen fixed overlay. Because posts live inside the virtualized post
list, whose `will-change: transform` establishes a containing block for
fixed-position descendants, the overlay anchored to the scrolled post list
content instead of the viewport. For longer channels/threads this pushed the
menu off screen (to the top of the message pane) so it appeared to do nothing.

Portal the mobile Message actions menu to document.body so its fixed overlay
is positioned relative to the viewport, and extend the mobile overlay styles
to the portaled container.

Co-authored-by: mattermost-code <[email protected]>

* [MM-69726] Keep portaled Message actions menu open until item click resolves

The mobile Message actions menu is portaled to the body, so it lives outside
the MenuWrapper node in the DOM. MenuWrapper's capture-phase blur handler
treated clicks inside the portaled menu as outside clicks and closed the menu
before the item's onClick fired (synchronously under the legacy React root),
so selecting an item did nothing. Mark the portal and skip blur-close for
clicks inside it.

Co-authored-by: mattermost-code <[email protected]>

* [MM-69726] Add tests for portaled mobile Message actions menu

Cover that the ActionsMenu portals its menu to the body in mobile view and
renders inline on desktop, and that MenuWrapper's blur handling keeps a
portaled menu open while still closing on genuine outside clicks.

Co-authored-by: mattermost-code <[email protected]>

* [MM-69726] Add integration coverage for portaled menu item clicks

Add a test that clicking an item in the portaled mobile Message actions menu
runs its action (the exact behavior the blur-handler fix restored), and a TAB
keyboard test mirroring the click-based portal exception.

Co-authored-by: mattermost-code <[email protected]>

* [MM-69726] Clarify portaled menu item test name

Co-authored-by: mattermost-code <[email protected]>

* [MM-69726] Fix import order in actions_menu test

Co-authored-by: mattermost-code <[email protected]>

* [MM-69726] Move portaled-menu blur handling out of generic MenuWrapper

Replace the hardcoded [data-menu-portal] selector in MenuWrapper.closeOnBlur
with a generic, opt-in portalNodeRef prop. ActionsMenu now owns the portal
detail and passes its portal node ref, keeping the generic (deprecated) widget
free of consumer-specific conditions.

* [MM-69726] Satisfy jsx-max-props-per-line in menu wrapper test

---------

Co-authored-by: mattermost-code <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 15:29:01 -04:00
aa3f1cf9f7 fix(mmctl): lengthen sampledata passwords to meet FIPS minimum (#37867)
* fix(mmctl): lengthen sampledata passwords to meet FIPS minimum

Zero-pad user index to 3 digits in guest and regular-user password
format strings so all generated passwords are >= 14 characters,
matching PasswordFIPSMinimumLength. Update inject-test-data login
hint and add a regression test.

Co-authored-by: Julien Tant <[email protected]>

* Simplify sampledata password length test comment

Co-authored-by: Julien Tant <[email protected]>

* Use subtests in sampledata password length coverage

So a single require failure reports only that case instead of
aborting the rest of the matrix.

Co-authored-by: Julien Tant <[email protected]>

* Name default-user subtests as user for clarity

Co-authored-by: Julien Tant <[email protected]>

* Run sampledata password length subtests in parallel

Co-authored-by: Julien Tant <[email protected]>

* Empty commit to re-trigger CI

---------

Co-authored-by: Julien Tant <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 17:28:58 +00:00
4bc378a1e4 [MM-69849][MM-69850] Add New Attribute page: create Text/Select/Multiselect/Rank attributes with an options editor (#37726)
* [MM-69849] Let Card opt out of its expand animation

CardBody's measure-then-animate expand mechanism relies on a CSS
transitionend to clear its `expanding` class, but every current
`.console`-styled Card consumer disables that transition outright
(`.console .Card__body.expanding { transition: none; }`), so the
event never fires and content stays clipped until something else
(e.g. a child re-render) forces a reflow.

Add a `disableExpandAnimation` prop that skips the measure/animate
path entirely for cards that are always expanded from mount and never
toggle, and a fallback timer so the existing toggle-capable consumers
recover even without the prop.

* [MM-69849] Add New Attribute creation page for Text attributes

Global Attributes today only lists attributes created elsewhere
(Classification Markings, LDAP/SAML sync) -- there's no way to define
a new one from the admin console itself. This adds that entry point
for the simplest case: a bare Text attribute, reusing the existing
generic PropertyField creation endpoint and CPA name-slugging/
validation logic already in place for Custom Profile Attributes, so
no server-side changes are needed.

Select/Multiselect/Rank types, the options editor, and the "Applies
to" targeting block are separate tickets and appear here only as
disabled/placeholder UI per the design spec.

* [MM-69849] Add e2e coverage for the create-attribute flow

Covers the full happy path (open the New Attribute page, type a
display name, verify the live-slugged unique name, edit/commit it
manually, save, confirm the new row) plus the reserved-word inline
error path that must block navigation without a round trip to the
server.

* [MM-69849] Fix prettier formatting in the create-attribute e2e spec

* [MM-69850] Unlock full Type selector and add options editor to New attribute

Select, Multiselect, and Rank are now selectable (previously locked to
Text with a "Coming soon" placeholder), with a real options editor:
free-form removable chips for Select/Multiselect and numbered chips for
Rank, both reordering via a keyboard-accessible "Move to position"
popover rather than drag-and-drop. Switching type before Save preserves
already-entered options. No server-side changes -- option sanitization
and Rank validation already run for this feature's access_control group.

* [MM-69850] Extract the invalid_options i18n key into en.json

Missed in the prior commit -- en.json was out of sync with the
optionsInvalid message added during review fixes, caught by the
pre-push i18n-extract:check hook.

* Revert CardBody's expand-animation fallback timer; keep options editor Save gated on committed options

CardBody's 350ms fallback timer changed rendered height/transition
behavior for every .console Card consumer, including five pre-existing
ones this branch doesn't touch or test (data retention settings, custom
and global policy forms, access control and permission policy details).
Reverted to the pre-MM-69849 behavior; disableExpandAnimation (used only
by this page) is untouched since it skips that code path entirely.

Also keeps Save on the New Attribute page disabled until an option is
actually committed via Enter/Tab/blur -- typing into the add-input alone
does not count, matching the existing chip-add convention elsewhere in
the codebase.

* Extract Global Attributes constants to their own module; polish the New attribute page's save-error banner and Name auto-derivation

Moves GLOBAL_ATTRIBUTES_GROUP_NAME/OBJECT_TYPE/TARGET_TYPE out of
global_attributes_table.tsx into a dedicated constants.ts so utils.ts
doesn't import them from a component file. Also fixes handleDoneClick
comparing against the auto-derived slug (instead of unconditionally
treating any non-empty manual edit as an override), surfaces the
server's own name-conflict message inline instead of a generic banner,
and caps the Display name input's length.

* Remove stale save_error.title i18n key left behind by the error-banner change

en.json wasn't regenerated when the previous commit removed the
saveErrorTitle message reference; re-running i18n-extract catches it.

* Address CodeRabbit review: honor isDisabled in AttributeDetails, fix CardBody stale-state blink, fix mock leakage in tests

- AttributeDetails ignored the isDisabled value SchemaAdminSettings forwards
  as `disabled`; a non-system-admin reaching the page could still edit and
  save. Threads disabled through the form controls, options editors, and
  canSave.
- CardBody's disableExpandAnimation branch never synced local expanded state,
  so re-enabling animation after expanded changed while disabled could render
  one frame with the stale value.
- utils.test.ts read `mock.calls[0]` without ever clearing mocks between
  tests, so later assertions were checking the first test's call args, not
  their own. Added jest.restoreAllMocks() there and in attribute_details.test.tsx.
- Added a length cap to the manual Name input and Select/Rank e2e coverage
  for the auto-derived unique name.

* Block Done/Enter while the manual Unique name is invalid

Previously "Done" (and Enter, which routes through the same handler)
committed whatever was typed, so a reserved word like "for" was left
sitting in the field behind a lingering error with Save disabled.

Done now refuses to commit an invalid Name. The block is expressed as
aria-disabled rather than the disabled attribute, so the button stays in
the tab order and a keyboard user lands on it and hears the reason via
aria-describedby instead of finding it silently gone.

This is not a focus trap -- there are three ways out: fix the name, clear
the field (an empty Name has no validation error, so Done goes live again
and applies its usual revert), or press Escape to discard the session.

* Address review: drop the inert JSX space in the Unique name caption

The `{' '}` between the "Unique name:" prefix and the value was a literal
space from before the caption became a flex container. Per the flexbox
spec a child text run containing only white space is not rendered, so it
became an anonymous flex item that gets dropped -- the 8px gap on
.AttributeDetails__uniqueNameCaption does the spacing. Removing it
changes nothing visually.

Four assertions read the caption's combined textContent, which does
include the whitespace node even though it never rendered, so they went
from 'Unique name: my_attribute' to 'Unique name:my_attributeEdit'.
Rescoped them to attributeUniqueNameValue, which is the element those
tests are actually about; the first one keeps a prefix check so the label
itself stays covered.

* Address review: move moveOptionByIndex into its own utils module

Its ranked counterpart, moveOptionByAscIndex, already lives in a pure
utils module (system_properties/rank_utils.ts), so the two reorder
helpers sat at different altitudes for no reason. option_utils.ts is the
unranked mirror of that file -- Select/Multiselect order is array
position, with no rank values to redistribute.

Not global_attributes/utils.ts: that one is API-payload-shaped and pulls
in Client4, which a plain array splice has no business importing.

Being addressable on its own also makes it directly testable, so this
adds the cases the component tests only covered incidentally: forward and
backward moves, the same-position no-op, that no rank field is
introduced, and that the input array is not mutated.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 10:16:15 -07:00
71447391a2 [MM-69601] mmctl: support file attachments in post create (#37310)
Co-authored-by: mattermost-code <[email protected]>
Co-authored-by: Doug Lauder <[email protected]>

Co-authored-by: Mattermost Build <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 16:21:18 +02:00
1dd5f77033 MM-70016: Fix edge case in team invitation handling (#37741)
* MM-70016: Fix edge case in team invitation handling

* Add test coverage for guest magic link invitation token

---------

Co-authored-by: Bill Gardner <[email protected]>
2026-08-10 23:06:24 +02:00
f0ed907983 [MM-70186] Add tooltips to platform icons in session attribute picker (#37898)
* [MM-70186] Add tooltips to platform icons in session attribute picker

Wrap the browser/desktop/mobile platform icons rendered as trailing
elements in the session attribute selector menu with WithTooltip so
hovering reveals the client context each icon represents.

Co-authored-by: mattermost-code <[email protected]>

* [MM-70186] Add tests for platform icon tooltips in attribute picker

Cover the accessible label mapping for each platform icon and assert the
Desktop/Mobile/Web Browser tooltips open on hover in the session
attribute selector menu.

Co-authored-by: mattermost-code <[email protected]>

* [MM-70186] Cover unknown-platform guard and simplify hover target

Add a case ensuring platforms without a matching icon are dropped while
known platforms still render, and target the icon's parent span directly
for the hover assertions.

Co-authored-by: mattermost-code <[email protected]>

* Add tooltips to platform icons in main Session Attributes table

- Wrap each platform icon in WithTooltip
- Update browser label from 'Browser' to 'Web Browser' for consistency with attribute selector
- Add hover tests to verify tooltip behavior

* Fix import order lint issue in platform_icons.test.tsx

* Refactor platform icons to use shared component and fix tooltip rendering

- Fix tooltip issue in session attributes table by wrapping icon in span
- Make PlatformIcons component flexible with variant prop (all-slots vs active-only)
- Export PLATFORM_ICONS and platformLabels for reuse
- Update attribute selector to use shared PlatformIcons component
- Remove duplicate icon definitions and i18n keys from attribute selector
- This eliminates code duplication and ensures consistent tooltip behavior

* Fix lint and type errors from shared PlatformIcons refactor

The shared-component refactor left attribute_selector_menu.tsx referencing
WithTooltip without an import (a runtime ReferenceError for synced/disallowed
attributes), kept now-unused ComponentType/defineMessages imports, and passed a
string[] to PlatformIcons which expects SessionPlatform[]. Restore the tooltip
import, drop the unused imports, and narrow platforms via getSessionAttrs.
Also apply eslint autofixes to platform_icons.tsx.

* Fix platform icon spacing by moving WithTooltip outside styled wrapper

- Move WithTooltip to wrap the outer span instead of adding inner wrapper
- This preserves the original CSS structure for both table and menu
- Fixes: table background now square with proper padding
- Fixes: menu icons maintain original spacing

* Fix dropdown menu icon spacing by rendering icons individually

- Replace PlatformIcons component usage in menu with individual icon rendering
- Each icon is now a separate element in trailingElements array
- Maintains proper spacing between icons in the dropdown menu
- Preserves shared PLATFORM_ICONS and platformLabels from platform_icons module

* Remove irrelevant package-lock.json changes

* Revert browser label from 'Web Browser' back to 'Browser'

- Keep the existing 'Browser' verbiage in i18n and all references
- Update platformLabels default message to 'Browser'
- Update all test assertions to expect 'Browser' instead of 'Web Browser'

---------

Co-authored-by: mattermost-code <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-10 20:24:14 +00:00
Devin BinnieandGitHub 5315eed279 [MM-70188] Convert the os_platform session attribute to a select field (#37901) 2026-08-10 16:03:20 -04:00
Edgar Bellot MicóandGitHub 7c0b56e833 MM-70072: Fix role validation for channel and team member updates (#37791)
* MM-70072: Fix role validation for channel and team member updates

* MM-70072: Assert specific error ID in guest/admin role regression tests

Make the new regression tests assert the specific error ID returned for
the guest+admin role combination, rather than only checking for a
generic bad-request status.
2026-08-10 15:40:13 -04:00