From a012af768d11b384686e24ac6319be4b2dfe3099 Mon Sep 17 00:00:00 2001 From: "cursor[bot]" <206951365+cursor[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:45:40 +0530 Subject: [PATCH 1/3] [MM-70246] Fix squished author avatar in Content Flagging RHS post preview card (#37972) * Fix squished author avatar in content flagging RHS post preview card The author avatar in the post message preview collapsed into a vertical sliver when the preview was rendered inside a constrained flex container (the Data Spillage / Content Flagging RHS report card). The avatar column shares the shrinkable .col__name flex rule (flex: 0 auto; min-width: 0), so it collapsed and dragged the min-width:0 avatar image down with it. Pin the avatar column (the .col__name containing .post__img) to its natural size so the round avatar keeps its correct dimensions. Co-authored-by: mattermost-code * Pin content flagging RHS preview avatar size to prevent squish The shared avatar rules (img.Avatar { min-width: 0 }) let the profile image shrink inside constrained flex layouts, so the round avatar in the content flagging RHS post preview card rendered as a squished sliver. Pin the preview avatar image to its intended 24px size (min-width + flex-shrink: 0) and keep its column from shrinking so it always stays round regardless of available width. * Remove redundant author-column flex rule in post header The preview-specific fix (min-width/flex-shrink on the avatar image plus flex: 0 0 auto on .post__img) already keeps the avatar round in constrained flex layouts, so the broader .col__name:has(.post__img) rule is no longer needed. Removing it also keeps the author text column shrinkable. --------- Co-authored-by: Cursor Agent Co-authored-by: mattermost-code --- webapp/channels/src/sass/components/_post.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/webapp/channels/src/sass/components/_post.scss b/webapp/channels/src/sass/components/_post.scss index 7a7dd08cfca..02e47161e66 100644 --- a/webapp/channels/src/sass/components/_post.scss +++ b/webapp/channels/src/sass/components/_post.scss @@ -1770,11 +1770,18 @@ .post__img { width: 24px; height: 24px; + flex: 0 0 auto; padding: 0; text-align: left; img.avatar-post-preview { display: block; + + // The shared avatar rules set min-width: 0, which lets the image + // shrink (and look squished) inside constrained flex layouts. + // Pin it to its intended size so it always stays round. + min-width: 24px; + flex-shrink: 0; } } From 505d6c1ecf4cb9d62bcc6a8ad100d0ba67c630c9 Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Mon, 17 Aug 2026 08:21:22 +0200 Subject: [PATCH 2/3] Add copy buttons for generated access tokens (#37494) Co-authored-by: Cursor Agent Co-authored-by: Ben Schumacher Co-authored-by: Mattermost Build --- .../components/integrations/bots/bot.test.tsx | 26 +++++++++++++++++++ .../src/components/integrations/bots/bot.tsx | 9 ++++++- .../user_access_token_section.test.tsx | 15 +++++++++++ .../user_access_token_section.tsx | 26 ++++++++++++++----- 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/webapp/channels/src/components/integrations/bots/bot.test.tsx b/webapp/channels/src/components/integrations/bots/bot.test.tsx index cef09113425..3fc4147c679 100644 --- a/webapp/channels/src/components/integrations/bots/bot.test.tsx +++ b/webapp/channels/src/components/integrations/bots/bot.test.tsx @@ -296,4 +296,30 @@ describe('components/integrations/bots/Bot', () => { expect(screen.queryByText(/^Disable$/)).not.toBeInTheDocument(); expect(screen.getByText(/^Enable$/)).toBeInTheDocument(); }); + + it('shows a copy button for a newly created token secret', async () => { + const bot = UtilsTestHelper.getBotMock({user_id: '1', owner_id: '1'}); + const owner = UtilsTestHelper.getUserMock({id: bot.owner_id}); + const user = UtilsTestHelper.getUserMock({id: bot.user_id}); + const createUserAccessToken = jest.fn().mockResolvedValue({data: {id: 'new-token-id', description: 'bot token', token: 'bot-secret'}}); + + renderWithContext( + , + ); + + fireEvent.click(screen.getByText('Create New Token')); + fireEvent.change(screen.getByLabelText('Token Description:'), {target: {value: 'bot token'}}); + fireEvent.click(screen.getByText('Save')); + + expect(await screen.findByText(/bot-secret/)).toBeInTheDocument(); + expect(screen.getByLabelText('Copy Token')).toBeInTheDocument(); + }); }); diff --git a/webapp/channels/src/components/integrations/bots/bot.tsx b/webapp/channels/src/components/integrations/bots/bot.tsx index 4a6e5666585..25cc22916bc 100644 --- a/webapp/channels/src/components/integrations/bots/bot.tsx +++ b/webapp/channels/src/components/integrations/bots/bot.tsx @@ -3,7 +3,7 @@ import React from 'react'; import type {ChangeEvent, SyntheticEvent, ReactNode} from 'react'; -import {FormattedMessage} from 'react-intl'; +import {defineMessage, FormattedMessage} from 'react-intl'; import {Link} from 'react-router-dom'; import {Button} from '@mattermost/shared/components/button'; @@ -14,12 +14,15 @@ import type {UserProfile, UserAccessToken} from '@mattermost/types/users'; import type {ActionResult} from 'mattermost-redux/types/actions'; import ConfirmModal from 'components/confirm_modal'; +import CopyText from 'components/copy_text'; import Markdown from 'components/markdown'; import SaveButton from 'components/save_button'; import WarningIcon from 'components/widgets/icons/fa_warning_icon'; import * as Utils from 'utils/utils'; +const copyTokenMessage = defineMessage({id: 'integrations.copy_token', defaultMessage: 'Copy Token'}); + export function matchesFilter(bot: BotType, filter?: string, owner?: UserProfile): boolean { if (!filter) { return true; @@ -471,6 +474,10 @@ export default class Bot extends React.PureComponent { /> {this.state.token.token} +
) : ( - - + + + {state.newToken!.token} + + - {state.newToken!.token} - + )} ), @@ -1130,6 +1138,10 @@ class UserAccessTokenSection extends React.PureComponent { /> {this.state.newToken!.token} + ); } else { From ea183fab48b833461d83af94c21cdc62eaef3d2d Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Mon, 17 Aug 2026 08:43:18 +0200 Subject: [PATCH 3/3] [MM-67157] Remove format parameter requirement from client license endpoint (#37167) * MM-67157: Remove unused format flag from /license/client endpoint The `format` query parameter on the `/license/client` (and local variant) endpoint was effectively dead: it was required but only ever accepted the single value `old`, returning an error otherwise. This mirrors the earlier removal of the same flag from `/config/client`, where the server now ignores the parameter while clients continue to send `format=old` for compatibility with pre-v11 servers. The server no longer inspects the `format` parameter, so requests with no format, `format=old`, or any other value all succeed. The unused i18n string and the parameter/response documentation in the OpenAPI spec are removed accordingly. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018Audz4JLNQN5SJxcPwyQBd * MM-67157: document format=old retention in webapp client Mirror the getClientConfig comment so the format=old query param on getClientLicenseOld is not mistakenly removed; clients keep sending it for compatibility with pre-v11 servers even though current servers now ignore it. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018Audz4JLNQN5SJxcPwyQBd * MM-67157: stop sending format=old from webapp client Now that the server ignores the format parameter on /license/client, drop format=old from the @mattermost/client getClientLicenseOld call and update the e2e intercepts/helpers that matched the old query string. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018Audz4JLNQN5SJxcPwyQBd * Drop unneeded wildcard from license/client cy.intercept path The format query param is gone from GET /license/client requests, so the trailing * used to match it is no longer needed. --------- Co-authored-by: Claude --- api/v4/source/system.yaml | 11 ----------- .../self-hosted/self_hosted_pricing_modal_spec.ts | 2 +- .../system_console/about/license_preview_spec.js | 8 ++++---- e2e-tests/cypress/tests/support/api/system.js | 2 +- server/channels/api4/license.go | 12 ------------ server/channels/api4/license_local.go | 12 ------------ server/channels/api4/license_test.go | 10 ++++------ server/i18n/en.json | 4 ---- webapp/platform/client/src/client4.ts | 2 +- 9 files changed, 11 insertions(+), 52 deletions(-) diff --git a/api/v4/source/system.yaml b/api/v4/source/system.yaml index 8b1e5a59dc4..1bf3208dca1 100644 --- a/api/v4/source/system.yaml +++ b/api/v4/source/system.yaml @@ -1013,20 +1013,9 @@ No permission required but having the `manage_system` permission returns more information. operationId: GetClientLicense - parameters: - - name: format - in: query - required: true - description: Must be `old`, other formats not implemented yet - schema: - type: string responses: "200": description: License retrieval successful - "400": - $ref: "#/components/responses/BadRequest" - "501": - $ref: "#/components/responses/NotImplemented" /api/v4/license/load_metric: get: tags: diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts index fe8c2eb00de..5dfb4b39c86 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts @@ -183,7 +183,7 @@ describe('Self hosted Pricing modal', () => { } function withTrialLicense(trial: string) { - cy.intercept('GET', '**/api/v4/license/client?format=old', { + cy.intercept('GET', '**/api/v4/license/client', { statusCode: 200, body: { IsLicensed: 'true', diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/license_preview_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/license_preview_spec.js index f441ef05c98..cf869bb62e9 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/license_preview_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/license_preview_spec.js @@ -22,7 +22,7 @@ import * as TIMEOUTS from '@/fixtures/timeouts'; const OLD_LICENSE_ID = 'old-license-id-0000000000000'; -// Current (already applied) license, as returned by GET /license/client?format=old. +// Current (already applied) license, as returned by GET /license/client. const currentClientLicense = buildClientLicense({ id: OLD_LICENSE_ID, skuName: 'Professional', @@ -57,7 +57,7 @@ describe('System console - License preview/diff view', () => { // mutable holder so we can simulate the server only reporting the new // license once propagation completes (by the time the modal closes). const clientLicenseHolder = {body: currentClientLicense}; - cy.intercept('GET', '**/api/v4/license/client?format=old', (req) => { + cy.intercept('GET', '**/api/v4/license/client', (req) => { req.reply({statusCode: 200, body: clientLicenseHolder.body}); }).as('getClientLicense'); @@ -121,7 +121,7 @@ describe('System console - License preview/diff view', () => { it('MM-67113 - Warns when re-uploading the currently applied license and leaves it unchanged', () => { // # The displayed license stays Professional throughout this flow - cy.intercept('GET', '**/api/v4/license/client?format=old', { + cy.intercept('GET', '**/api/v4/license/client', { statusCode: 200, body: currentClientLicense, }).as('getClientLicense'); @@ -222,7 +222,7 @@ function buildLicense({id, skuName, skuShortName, users}) { }; } -// Build a ClientLicense object (old format) matching GET /license/client?format=old. +// Build a ClientLicense object (old format) matching GET /license/client. // All values are strings, as produced by the server. function buildClientLicense({id, skuName, skuShortName, users}) { const now = Date.now(); diff --git a/e2e-tests/cypress/tests/support/api/system.js b/e2e-tests/cypress/tests/support/api/system.js index 68b3b38ce08..320758bbcfc 100644 --- a/e2e-tests/cypress/tests/support/api/system.js +++ b/e2e-tests/cypress/tests/support/api/system.js @@ -27,7 +27,7 @@ function hasLicenseForFeature(license, key) { } Cypress.Commands.add('apiGetClientLicense', () => { - return cy.request('/api/v4/license/client?format=old').then((response) => { + return cy.request('/api/v4/license/client').then((response) => { expect(response.status).to.equal(200); const license = response.body; diff --git a/server/channels/api4/license.go b/server/channels/api4/license.go index ac89f0d4312..37c5ebfda05 100644 --- a/server/channels/api4/license.go +++ b/server/channels/api4/license.go @@ -28,18 +28,6 @@ func (api *API) InitLicense() { } func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) { - format := r.URL.Query().Get("format") - - if format == "" { - c.Err = model.NewAppError("getClientLicense", "api.license.client.old_format.app_error", nil, "", http.StatusBadRequest) - return - } - - if format != "old" { - c.SetInvalidParam("format") - return - } - var clientLicense map[string]string if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) { diff --git a/server/channels/api4/license_local.go b/server/channels/api4/license_local.go index 08bd74dbcc7..524d84e32ee 100644 --- a/server/channels/api4/license_local.go +++ b/server/channels/api4/license_local.go @@ -103,18 +103,6 @@ func localRemoveLicense(c *Context, w http.ResponseWriter, r *http.Request) { } func localGetClientLicense(c *Context, w http.ResponseWriter, r *http.Request) { - format := r.URL.Query().Get("format") - - if format == "" { - c.Err = model.NewAppError("localGetClientLicense", "api.license.client.old_format.app_error", nil, "", http.StatusBadRequest) - return - } - - if format != "old" { - c.SetInvalidParam("format") - return - } - clientLicense := c.App.Srv().ClientLicense() if _, err := w.Write([]byte(model.MapToJSON(clientLicense))); err != nil { diff --git a/server/channels/api4/license_test.go b/server/channels/api4/license_test.go index 61ab4407b36..f15f2bca489 100644 --- a/server/channels/api4/license_test.go +++ b/server/channels/api4/license_test.go @@ -38,14 +38,12 @@ func TestGetOldClientLicense(t *testing.T) { require.NoError(t, err) resp, err := client.DoAPIGet(context.Background(), "/license/client", "") - require.Error(t, err, "get /license/client did not return an error") - require.Equal(t, http.StatusBadRequest, resp.StatusCode, - "expected 400 bad request") + require.NoError(t, err, "get /license/client should not return an error") + require.Equal(t, http.StatusOK, resp.StatusCode, "expected 200 OK") resp, err = client.DoAPIGet(context.Background(), "/license/client?format=junk", "") - require.Error(t, err, "get /license/client?format=junk did not return an error") - require.Equal(t, http.StatusBadRequest, resp.StatusCode, - "expected 400 Bad Request") + require.NoError(t, err, "get /license/client?format=junk should not return an error") + require.Equal(t, http.StatusOK, resp.StatusCode, "expected 200 OK") license, _, err = th.SystemAdminClient.GetOldClientLicense(context.Background(), "") require.NoError(t, err) diff --git a/server/i18n/en.json b/server/i18n/en.json index ae2b6842a3e..9b6bfc1ab1c 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -2700,10 +2700,6 @@ "id": "api.license.add_license.wrong_environment_test.app_error", "translation": "This is a test or development license, but this server is running in a production environment. Test or development licenses can only be used on test or development servers." }, - { - "id": "api.license.client.old_format.app_error", - "translation": "New format for the client license is not supported yet. Please specify format=old in the query string." - }, { "id": "api.license.load_metric.app_error", "translation": "Failed to compute monthly active users." diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index e621332a877..aad9668d270 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -3068,7 +3068,7 @@ export default class Client4 { getClientLicenseOld = () => { return this.doFetch( - `${this.getBaseRoute()}/license/client?format=old`, + `${this.getBaseRoute()}/license/client`, {method: 'get'}, ); };