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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
* 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]>
* 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]>
* 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]>
* 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]>
* [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]>
* 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
* 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]>
* [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]>
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]>
* 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]>
* 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]>
* 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.
* [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]>
* 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]>
* [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]>
* 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]>
* [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]>
* 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.