Merge branch 'master' into MM-52097
@@ -98,6 +98,72 @@ describe('Create and delete board / card', () => {
|
||||
cy.findByText('for testing purposes only').should('be.visible');
|
||||
});
|
||||
|
||||
it('MM-T4276 Set up Board emoji', () => {
|
||||
cy.visit('/boards');
|
||||
|
||||
// # Create an empty board and change tile to Testing
|
||||
cy.findByText('Create an empty board').should('exist').click({force: true});
|
||||
cy.get('.BoardComponent').should('exist');
|
||||
|
||||
// # Change Title
|
||||
cy.findByPlaceholderText('Untitled board').should('be.visible').wait(timeouts.HALF_SEC);
|
||||
|
||||
// * Assert that the title is changed to "testing"
|
||||
cy.findByPlaceholderText('Untitled board').
|
||||
clear().
|
||||
type('Testing').
|
||||
type('{enter}').
|
||||
should('have.value', 'Testing');
|
||||
|
||||
// # "Add icon" and "Show description" options appear
|
||||
cy.findByText('Add icon').should('exist');
|
||||
cy.findByText('show description').should('exist');
|
||||
|
||||
// # Click on "Add icon"
|
||||
cy.findByText('Add icon').should('exist').click({force: true});
|
||||
|
||||
// * Assert that a random emoji is selected and added at the beginning of the board title
|
||||
cy.get('.IconSelector').should('exist');
|
||||
|
||||
// # Click on the emoji next to the board title
|
||||
cy.get('.IconSelector .MenuWrapper').should('exist').click({force: true});
|
||||
|
||||
// * Assert that Dropdown menu with 3 options appears
|
||||
cy.findByText('Random').should('exist');
|
||||
cy.findByText('Pick icon').should('exist');
|
||||
cy.findByText('Remove icon').should('exist');
|
||||
|
||||
// # Hover your mouse over the "Pick Icon" option
|
||||
cy.findByText('Pick icon').trigger('mouseover');
|
||||
|
||||
// * Assert that emoji picker menu appears
|
||||
cy.get('.IconSelector .menu-contents').should('exist');
|
||||
|
||||
// # Click on the emoji from the picker
|
||||
cy.get('.EmojiPicker').should('exist').and('be.visible').within(() => {
|
||||
// # Click on the emoji
|
||||
cy.get("[aria-label='😀, grinning']").should('exist');
|
||||
cy.get("[aria-label='😀, grinning']").eq(0).click({force: true});
|
||||
});
|
||||
|
||||
// * Assert that Selected emoji is now displayed next to the board title
|
||||
cy.get('.IconSelector span').contains('😀');
|
||||
|
||||
// # Click on the emoji next to the board title
|
||||
cy.get('.IconSelector .MenuWrapper').should('exist').click({force: true});
|
||||
|
||||
// * Assert that Dropdown menu with 3 options appears
|
||||
cy.findByText('Random').should('exist');
|
||||
cy.findByText('Pick icon').should('exist');
|
||||
cy.findByText('Remove icon').should('exist');
|
||||
|
||||
// # Click "Remove icon"
|
||||
cy.findByText('Remove icon').click({force: true});
|
||||
|
||||
// * Assert that Icon next to the board title is removed
|
||||
cy.get('.IconSelector').should('not.exist');
|
||||
});
|
||||
|
||||
it('MM-T5397 Can create and delete a board and a card', () => {
|
||||
// Visit a page and create new empty board
|
||||
cy.visit('/boards');
|
||||
|
||||
@@ -2,8 +2,21 @@
|
||||
|
||||
#### 1. Start local server in a separate terminal.
|
||||
|
||||
```
|
||||
# Typically run the local server with:
|
||||
cd server && make run
|
||||
|
||||
# Or build and distribute webapp including channels, boards and playbooks
|
||||
# so that their product URLs do not rely on Webpack dev server.
|
||||
# Especially important when running test inside the Playwright's docker container.
|
||||
cd webapp && make dist
|
||||
cd server && make run-server
|
||||
```
|
||||
|
||||
#### 2. Install dependencies and run the test.
|
||||
|
||||
Note: If you're using Node.js version 18 and above, you may need to set `NODE_OPTIONS='--no-experimental-fetch'`.
|
||||
|
||||
```
|
||||
# Install npm packages
|
||||
npm i
|
||||
@@ -32,14 +45,16 @@ npm run test
|
||||
Change to root directory, run docker container
|
||||
|
||||
```
|
||||
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.30.0-focal /bin/bash
|
||||
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.32.0-focal /bin/bash
|
||||
```
|
||||
|
||||
#### 2. Inside the docker container
|
||||
|
||||
```
|
||||
export NODE_OPTIONS='--no-experimental-fetch'
|
||||
export PW_BASE_URL=http://host.docker.internal:8065
|
||||
cd mattermost/e2e/playwright
|
||||
export PW_HEADLESS=true
|
||||
cd mattermost/e2e-tests/playwright
|
||||
|
||||
# Install npm packages. Use "npm ci" to match the automated environment
|
||||
npm ci
|
||||
|
||||
991
e2e-tests/playwright/package-lock.json
сгенерированный
@@ -1,33 +1,35 @@
|
||||
{
|
||||
"scripts": {
|
||||
"test": "PW_SNAPSHOT_ENABLE=true playwright test",
|
||||
"percy": "PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=iphone --project=ipad",
|
||||
"test": "cross-env PW_SNAPSHOT_ENABLE=true playwright test",
|
||||
"percy": "cross-env PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=iphone --project=ipad",
|
||||
"tsc": "tsc -b",
|
||||
"lint": "eslint . --ext .js,.ts",
|
||||
"prettier": "prettier --write .",
|
||||
"check": "npm run tsc && npm run lint && npm run prettier",
|
||||
"codegen": "playwright codegen $PW_BASE_URL",
|
||||
"test-slomo": "PW_SNAPSHOT_ENABLE=true PW_HEADLESS=false PW_SLOWMO=1000 playwright test",
|
||||
"codegen": "cross-env playwright codegen $PW_BASE_URL",
|
||||
"playwright-ui": "playwright test --ui",
|
||||
"test-slomo": "cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test",
|
||||
"show-report": "npx playwright show-report"
|
||||
},
|
||||
"dependencies": {
|
||||
"@percy/cli": "1.18.0",
|
||||
"@percy/cli": "1.23.0",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.32.3",
|
||||
"async-wait-until": "2.0.12",
|
||||
"chalk": "4.1.2",
|
||||
"deepmerge": "4.3.0",
|
||||
"deepmerge": "4.3.1",
|
||||
"dotenv": "16.0.3",
|
||||
"form-data": "4.0.0",
|
||||
"isomorphic-unfetch": "4.0.2",
|
||||
"uuid": "9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.51.0",
|
||||
"@typescript-eslint/parser": "5.51.0",
|
||||
"eslint": "8.34.0",
|
||||
"prettier": "2.8.4",
|
||||
"typescript": "4.9.5"
|
||||
"@types/uuid": "9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.0",
|
||||
"@typescript-eslint/parser": "5.59.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.38.0",
|
||||
"prettier": "2.8.7",
|
||||
"typescript": "5.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
# - Default to "false" if not set.
|
||||
|
||||
# 12. PW_HEADLESS
|
||||
# - Default to "true" if not set. Set to false to run test in head mode.
|
||||
# - Default to "false" or headless mode if not set. Set to true to run test in headed mode.
|
||||
|
||||
# 13. PW_SLOWMO
|
||||
# - Default to "0" if not set which means normal test speed run. Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
|
||||
import {writeFile} from 'node:fs/promises';
|
||||
|
||||
import {request, Browser} from '@playwright/test';
|
||||
import {request, Browser, BrowserContext} from '@playwright/test';
|
||||
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
import testConfig from '@e2e-test.config';
|
||||
|
||||
export class TestBrowser {
|
||||
readonly browser: Browser;
|
||||
context: BrowserContext | null;
|
||||
|
||||
constructor(browser: Browser) {
|
||||
this.browser = browser;
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
async login(user: UserProfile | null) {
|
||||
@@ -27,8 +29,16 @@ export class TestBrowser {
|
||||
const context = await this.browser.newContext(options);
|
||||
const page = await context.newPage();
|
||||
|
||||
this.context = context;
|
||||
|
||||
return {context, page};
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.context) {
|
||||
await this.context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginByAPI(loginId: string, password: string, token = '', ldapOnly = false) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// This is based on "packages/client/src/client4.ts". Modified for node client.
|
||||
// This is based on "webapp/platform/client/src/client4.ts". Modified for node client.
|
||||
// Update should be made in comparison with the base Client4.
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -134,7 +134,7 @@ export default class Client extends Client4 {
|
||||
|
||||
// *****************************************************************************
|
||||
// Boards client
|
||||
// based on https://github.com/mattermost/focalboard/blob/main/webapp/src/octoClient.ts
|
||||
// based on "webapp/boards/src/octoClient.ts"
|
||||
// *****************************************************************************
|
||||
|
||||
async patchUserConfig(userID: string, patch: UserConfigPatch): Promise<UserPreference[] | undefined> {
|
||||
|
||||
@@ -319,7 +319,6 @@ const defaultServerConfig: AdminConfig = {
|
||||
LoginButtonColor: '#0000',
|
||||
LoginButtonBorderColor: '#2389D7',
|
||||
LoginButtonTextColor: '#2389D7',
|
||||
EnableInactivityEmail: true,
|
||||
},
|
||||
RateLimitSettings: {
|
||||
Enable: false,
|
||||
@@ -533,6 +532,7 @@ const defaultServerConfig: AdminConfig = {
|
||||
EnableRemoteClusterService: false,
|
||||
EnableAppBar: false,
|
||||
PatchPluginsReactDOM: false,
|
||||
DisableRefetchingOnBrowserFocus: false,
|
||||
},
|
||||
AnalyticsSettings: {
|
||||
MaxUsersForStatistics: 2500,
|
||||
@@ -622,12 +622,6 @@ const defaultServerConfig: AdminConfig = {
|
||||
'com.mattermost.nps': {
|
||||
Enable: true,
|
||||
},
|
||||
focalboard: {
|
||||
Enable: true,
|
||||
},
|
||||
playbooks: {
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
EnableMarketplace: true,
|
||||
EnableRemoteMarketplace: true,
|
||||
@@ -671,13 +665,12 @@ const defaultServerConfig: AdminConfig = {
|
||||
BoardsFeatureFlags: '',
|
||||
BoardsDataRetention: false,
|
||||
NormalizeLdapDNs: false,
|
||||
EnableInactivityCheckJob: true,
|
||||
UseCaseOnboarding: true,
|
||||
GraphQL: false,
|
||||
InsightsEnabled: true,
|
||||
CommandPalette: false,
|
||||
SendWelcomePost: true,
|
||||
WorkTemplate: false,
|
||||
WorkTemplate: true,
|
||||
PostPriority: true,
|
||||
WysiwygEditor: false,
|
||||
PeopleProduct: false,
|
||||
@@ -686,7 +679,9 @@ const defaultServerConfig: AdminConfig = {
|
||||
ThreadsEverywhere: false,
|
||||
GlobalDrafts: true,
|
||||
OnboardingTourTips: true,
|
||||
DeprecateCloudFree: false,
|
||||
AppsSidebarCategory: false,
|
||||
CloudReverseTrial: false,
|
||||
},
|
||||
ImportSettings: {
|
||||
Directory: './import',
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
import path from 'node:path';
|
||||
import {expect} from '@playwright/test';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {ClientError} from '@mattermost/client/client4';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
import testConfig from '@e2e-test.config';
|
||||
|
||||
@@ -77,10 +79,21 @@ export async function initSetup({
|
||||
offTopicUrl: getUrl(team.name, 'off-topic'),
|
||||
townSquareUrl: getUrl(team.name, 'town-square'),
|
||||
};
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
// log an error for debugging
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err);
|
||||
const err = error as ClientError;
|
||||
if (err.message === 'Could not parse multipart form.') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.yellow(`node version: ${process.version}\nNODE_OPTIONS: ${process.env.NODE_OPTIONS}`));
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
chalk.green(
|
||||
`This failed due to the experimental fetch support in Node.js starting v18.0.0.\nYou may set environment variable: "export NODE_OPTIONS='--no-experimental-fetch'", then try again.'`
|
||||
)
|
||||
);
|
||||
}
|
||||
expect(err, 'Should not throw an error').toBeFalsy();
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export const test = base.extend<ExtendedFixtures>({
|
||||
pw: async ({browser}, use) => {
|
||||
const pw = new PlaywrightExtended(browser);
|
||||
await use(pw);
|
||||
await pw.testBrowser.close();
|
||||
},
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
pages: async ({}, use) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ export default class GlobalHeader {
|
||||
|
||||
async switchProduct(name: string) {
|
||||
await this.productSwitchMenu.click();
|
||||
await this.container.getByRole('link', {name: ` ${name}`}).click();
|
||||
await this.container.getByRole('link', {name}).click();
|
||||
}
|
||||
|
||||
async toBeVisible(name: string) {
|
||||
|
||||
@@ -55,7 +55,7 @@ const config: TestConfig = {
|
||||
// CI
|
||||
isCI: !!process.env.CI,
|
||||
// Playwright
|
||||
headless: parseBool(process.env.PW_HEADLESS, false),
|
||||
headless: parseBool(process.env.PW_HEADLESS, true),
|
||||
slowMo: parseNumber(process.env.PW_SLOWMO, 0),
|
||||
workers: parseNumber(process.env.PW_WORKERS, 1),
|
||||
// Visual tests
|
||||
|
||||
|
До Ширина: | Высота: | Размер: 129 KiB После Ширина: | Высота: | Размер: 127 KiB |
|
До Ширина: | Высота: | Размер: 184 KiB После Ширина: | Высота: | Размер: 182 KiB |
|
До Ширина: | Высота: | Размер: 246 KiB После Ширина: | Высота: | Размер: 238 KiB |
|
До Ширина: | Высота: | Размер: 38 KiB После Ширина: | Высота: | Размер: 38 KiB |
|
До Ширина: | Высота: | Размер: 65 KiB После Ширина: | Высота: | Размер: 63 KiB |
|
До Ширина: | Высота: | Размер: 85 KiB После Ширина: | Высота: | Размер: 84 KiB |
|
До Ширина: | Высота: | Размер: 77 KiB После Ширина: | Высота: | Размер: 71 KiB |
|
До Ширина: | Высота: | Размер: 113 KiB После Ширина: | Высота: | Размер: 106 KiB |
|
До Ширина: | Высота: | Размер: 183 KiB После Ширина: | Высота: | Размер: 167 KiB |
|
До Ширина: | Высота: | Размер: 178 KiB После Ширина: | Высота: | Размер: 148 KiB |
|
До Ширина: | Высота: | Размер: 105 KiB После Ширина: | Высота: | Размер: 105 KiB |
|
До Ширина: | Высота: | Размер: 169 KiB После Ширина: | Высота: | Размер: 170 KiB |
|
До Ширина: | Высота: | Размер: 212 KiB После Ширина: | Высота: | Размер: 213 KiB |
|
До Ширина: | Высота: | Размер: 157 KiB После Ширина: | Высота: | Размер: 156 KiB |
|
До Ширина: | Высота: | Размер: 150 KiB После Ширина: | Высота: | Размер: 150 KiB |
|
До Ширина: | Высота: | Размер: 147 KiB После Ширина: | Высота: | Размер: 147 KiB |
|
До Ширина: | Высота: | Размер: 270 KiB После Ширина: | Высота: | Размер: 271 KiB |
|
До Ширина: | Высота: | Размер: 312 KiB После Ширина: | Высота: | Размер: 312 KiB |
|
До Ширина: | Высота: | Размер: 241 KiB После Ширина: | Высота: | Размер: 241 KiB |
|
До Ширина: | Высота: | Размер: 276 KiB После Ширина: | Высота: | Размер: 277 KiB |
|
До Ширина: | Высота: | Размер: 297 KiB После Ширина: | Высота: | Размер: 297 KiB |
|
До Ширина: | Высота: | Размер: 230 KiB После Ширина: | Высота: | Размер: 230 KiB |
|
До Ширина: | Высота: | Размер: 156 KiB После Ширина: | Высота: | Размер: 165 KiB |
|
До Ширина: | Высота: | Размер: 160 KiB После Ширина: | Высота: | Размер: 170 KiB |
|
До Ширина: | Высота: | Размер: 285 KiB После Ширина: | Высота: | Размер: 296 KiB |
|
До Ширина: | Высота: | Размер: 368 KiB После Ширина: | Высота: | Размер: 411 KiB |
|
До Ширина: | Высота: | Размер: 276 KiB После Ширина: | Высота: | Размер: 355 KiB |
|
До Ширина: | Высота: | Размер: 280 KiB После Ширина: | Высота: | Размер: 291 KiB |
|
До Ширина: | Высота: | Размер: 348 KiB После Ширина: | Высота: | Размер: 393 KiB |
|
До Ширина: | Высота: | Размер: 256 KiB После Ширина: | Высота: | Размер: 266 KiB |
@@ -138,7 +138,7 @@ TEMPLATES_DIR=templates
|
||||
|
||||
# Plugins Packages
|
||||
PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v0.1.2
|
||||
PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.2.2
|
||||
PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.4.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-calls-v0.15.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.0.0
|
||||
|
||||
@@ -8,7 +8,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
ENV PATH="/mattermost/bin:${PATH}"
|
||||
ARG PUID=2000
|
||||
ARG PGID=2000
|
||||
ARG MM_PACKAGE="https://releases.mattermost.com/7.9.2/mattermost-7.9.2-linux-amd64.tar.gz?src=docker"
|
||||
ARG MM_PACKAGE="https://releases.mattermost.com/7.10.0/mattermost-7.10.0-linux-amd64.tar.gz?src=docker"
|
||||
|
||||
# # Install needed packages and indirect dependencies
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -323,7 +323,14 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "command_args", &commandArgs)
|
||||
|
||||
// checks that user is a member of the specified channel, and that they have permission to use slash commands in it
|
||||
// Checks that user is a member of the specified channel, and that they have permission to create a post in it.
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionCreatePost) {
|
||||
c.SetPermissionError(model.PermissionCreatePost)
|
||||
return
|
||||
}
|
||||
|
||||
// For compatibility reasons, PermissionCreatePost is also checked.
|
||||
// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionUseSlashCommands) {
|
||||
c.SetPermissionError(model.PermissionUseSlashCommands)
|
||||
return
|
||||
@@ -343,6 +350,13 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that
|
||||
// they can't just execute slash commands against arbitrary teams
|
||||
if c.AppContext.Session().GetTeamByTeamId(commandArgs.TeamId) == nil {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreatePost) {
|
||||
c.SetPermissionError(model.PermissionCreatePost)
|
||||
return
|
||||
}
|
||||
|
||||
// For compatibility reasons, PermissionCreatePost is also checked.
|
||||
// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionUseSlashCommands) {
|
||||
c.SetPermissionError(model.PermissionUseSlashCommands)
|
||||
return
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
@@ -1065,3 +1066,80 @@ func TestExecuteCommandInTeamUserIsNotOn(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestExecuteCommandReadOnly(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
ctx := request.EmptyContext(th.TestLogger)
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
enableCommands := *th.App.Config().ServiceSettings.EnableCommands
|
||||
allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
|
||||
})
|
||||
}()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
||||
})
|
||||
|
||||
expectedCommandResponse := &model.CommandResponse{
|
||||
Text: "test post command response",
|
||||
ResponseType: model.CommandResponseTypeInChannel,
|
||||
Type: "custom_test",
|
||||
Props: map[string]any{"someprop": "somevalue"},
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
r.ParseForm()
|
||||
require.Equal(t, th.BasicTeam.Name, r.FormValue("team_domain"))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(expectedCommandResponse); err != nil {
|
||||
th.TestLogger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// create a slash command on that team
|
||||
postCmd := &model.Command{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
URL: ts.URL,
|
||||
Method: model.CommandMethodPost,
|
||||
Trigger: "postcommand",
|
||||
}
|
||||
_, appErr := th.App.CreateCommand(postCmd)
|
||||
require.Nil(t, appErr, "failed to create post command")
|
||||
|
||||
// Confirm that the command works when the channel is not read only
|
||||
_, resp, err := client.ExecuteCommandWithTeam(th.BasicChannel.Id, th.BasicChannel.TeamId, "/postcommand")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
// Enable Enterprise features
|
||||
th.App.Srv().SetLicense(model.NewTestLicense())
|
||||
|
||||
th.App.SetPhase2PermissionsMigrationStatus(true)
|
||||
|
||||
_, appErr = th.App.PatchChannelModerationsForChannel(
|
||||
ctx,
|
||||
th.BasicChannel,
|
||||
[]*model.ChannelModerationPatch{{
|
||||
Name: &model.PermissionCreatePost.Id,
|
||||
Roles: &model.ChannelModeratedRolesPatch{
|
||||
Guests: model.NewBool(false),
|
||||
Members: model.NewBool(false),
|
||||
},
|
||||
}})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Confirm that the command fails when the channel is read only
|
||||
_, resp, err = client.ExecuteCommandWithTeam(th.BasicChannel.Id, th.BasicChannel.TeamId, "/postcommand")
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
|
||||
|
||||
props["CWSURL"] = *c.CloudSettings.CWSURL
|
||||
|
||||
props["DisableRefetchingOnBrowserFocus"] = strconv.FormatBool(*c.ExperimentalSettings.DisableRefetchingOnBrowserFocus)
|
||||
|
||||
// Set default values for all options that require a license.
|
||||
props["ExperimentalEnableAuthenticationTransfer"] = "true"
|
||||
props["LdapNicknameAttributeSet"] = "false"
|
||||
|
||||
@@ -979,6 +979,7 @@ type ExperimentalSettings struct {
|
||||
EnableRemoteClusterService *bool `access:"experimental_features"`
|
||||
EnableAppBar *bool `access:"experimental_features"`
|
||||
PatchPluginsReactDOM *bool `access:"experimental_features"`
|
||||
DisableRefetchingOnBrowserFocus *bool `access:"experimental_features"`
|
||||
}
|
||||
|
||||
func (s *ExperimentalSettings) SetDefaults() {
|
||||
@@ -1017,6 +1018,10 @@ func (s *ExperimentalSettings) SetDefaults() {
|
||||
if s.PatchPluginsReactDOM == nil {
|
||||
s.PatchPluginsReactDOM = NewBool(false)
|
||||
}
|
||||
|
||||
if s.DisableRefetchingOnBrowserFocus == nil {
|
||||
s.DisableRefetchingOnBrowserFocus = NewBool(false)
|
||||
}
|
||||
}
|
||||
|
||||
type AnalyticsSettings struct {
|
||||
|
||||
@@ -21,6 +21,9 @@ type Permission struct {
|
||||
|
||||
var PermissionInviteUser *Permission
|
||||
var PermissionAddUserToTeam *Permission
|
||||
|
||||
// Deprecated: PermissionCreatePost should be used to determine if a slash command can be executed.
|
||||
// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274
|
||||
var PermissionUseSlashCommands *Permission
|
||||
var PermissionManageSlashCommands *Permission
|
||||
var PermissionManageOthersSlashCommands *Permission
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
// It should be maintained in chronological order with most current
|
||||
// release at the front of the list.
|
||||
var versions = []string{
|
||||
"7.11.0",
|
||||
"7.10.0",
|
||||
"7.9.0",
|
||||
"7.8.0",
|
||||
|
||||
@@ -750,15 +750,16 @@ func (ts *TelemetryService) trackConfig() {
|
||||
})
|
||||
|
||||
ts.SendTelemetry(TrackConfigExperimental, map[string]any{
|
||||
"client_side_cert_enable": *cfg.ExperimentalSettings.ClientSideCertEnable,
|
||||
"isdefault_client_side_cert_check": isDefault(*cfg.ExperimentalSettings.ClientSideCertCheck, model.ClientSideCertCheckPrimaryAuth),
|
||||
"link_metadata_timeout_milliseconds": *cfg.ExperimentalSettings.LinkMetadataTimeoutMilliseconds,
|
||||
"restrict_system_admin": *cfg.ExperimentalSettings.RestrictSystemAdmin,
|
||||
"use_new_saml_library": *cfg.ExperimentalSettings.UseNewSAMLLibrary,
|
||||
"enable_shared_channels": *cfg.ExperimentalSettings.EnableSharedChannels,
|
||||
"enable_remote_cluster_service": *cfg.ExperimentalSettings.EnableRemoteClusterService && cfg.FeatureFlags.EnableRemoteClusterService,
|
||||
"enable_app_bar": *cfg.ExperimentalSettings.EnableAppBar,
|
||||
"patch_plugins_react_dom": *cfg.ExperimentalSettings.PatchPluginsReactDOM,
|
||||
"client_side_cert_enable": *cfg.ExperimentalSettings.ClientSideCertEnable,
|
||||
"isdefault_client_side_cert_check": isDefault(*cfg.ExperimentalSettings.ClientSideCertCheck, model.ClientSideCertCheckPrimaryAuth),
|
||||
"link_metadata_timeout_milliseconds": *cfg.ExperimentalSettings.LinkMetadataTimeoutMilliseconds,
|
||||
"restrict_system_admin": *cfg.ExperimentalSettings.RestrictSystemAdmin,
|
||||
"use_new_saml_library": *cfg.ExperimentalSettings.UseNewSAMLLibrary,
|
||||
"enable_shared_channels": *cfg.ExperimentalSettings.EnableSharedChannels,
|
||||
"enable_remote_cluster_service": *cfg.ExperimentalSettings.EnableRemoteClusterService && cfg.FeatureFlags.EnableRemoteClusterService,
|
||||
"enable_app_bar": *cfg.ExperimentalSettings.EnableAppBar,
|
||||
"patch_plugins_react_dom": *cfg.ExperimentalSettings.PatchPluginsReactDOM,
|
||||
"disable_refetching_on_browser_focus": *cfg.ExperimentalSettings.DisableRefetchingOnBrowserFocus,
|
||||
})
|
||||
|
||||
ts.SendTelemetry(TrackConfigAnalytics, map[string]any{
|
||||
|
||||
@@ -6887,6 +6887,15 @@ const AdminDefinition = {
|
||||
isHidden: it.licensedForFeature('Cloud'),
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
{
|
||||
type: Constants.SettingsTypes.TYPE_BOOL,
|
||||
key: 'ExperimentalSettings.DisableRefetchingOnBrowserFocus',
|
||||
label: t('admin.experimental.disableRefetchingOnBrowserFocus.title'),
|
||||
label_default: 'Disable data refetching on browser refocus:',
|
||||
help_text: t('admin.experimental.disableRefetchingOnBrowserFocus.desc'),
|
||||
help_text_default: 'When true, Mattermost will not refetch channels and channel members when the browser regains focus. This may result in improved performance for users with many channels and channel members.',
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -38,7 +38,11 @@ export default class GeneratedSetting extends React.PureComponent<Props> {
|
||||
private regenerate = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onChange(this.props.id, crypto.randomBytes(256).toString('base64').substring(0, 32));
|
||||
// Pure base64 implementation can contain characters that are not URL safe without additional
|
||||
// encoding. Adopt a URL/Filename safer alphabet as noted in https://datatracker.ietf.org/doc/html/rfc4648#section-5
|
||||
// where: 62 - (minus) , 63 _ (underscore)
|
||||
const value = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||
this.props.onChange(this.props.id, value.replaceAll('+', '-').replaceAll('/', '_'));
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
||||
@@ -90,7 +90,7 @@ export default class AppsFormSelectField extends React.PureComponent<Props, Stat
|
||||
loadDynamicUserOptions = async (userInput: string): Promise<AppSelectOption[]> => {
|
||||
const usersSearchResults: UserAutocomplete = await this.props.actions.autocompleteUsers(userInput.toLowerCase());
|
||||
|
||||
return usersSearchResults.users.map((user) => {
|
||||
return usersSearchResults.users.filter((user) => !user.is_bot).map((user) => {
|
||||
const label = this.props.teammateNameDisplay ? displayUsername(user, this.props.teammateNameDisplay) : user.username;
|
||||
|
||||
return {...user, label, value: user.id, icon_data: imageURLForUser(user.id)};
|
||||
|
||||
@@ -36,6 +36,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
const currentUser = getCurrentUser(state);
|
||||
const plugins = state.plugins.components.NeedsTeamComponent;
|
||||
const graphQLEnabled = isGraphQLEnabled(state);
|
||||
const disableRefetchingOnBrowserFocus = config.DisableRefetchingOnBrowserFocus === 'true';
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
@@ -46,6 +47,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
selectedThreadId: getSelectedThreadIdInCurrentTeam(state),
|
||||
mfaRequired: checkIfMFARequired(currentUser, license, config, ownProps.match.url),
|
||||
graphQLEnabled,
|
||||
disableRefetchingOnBrowserFocus,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ function TeamController(props: Props) {
|
||||
const wakeUpIntervalId = setInterval(() => {
|
||||
const currentTime = Date.now();
|
||||
if ((currentTime - lastTime.current) > WAKEUP_THRESHOLD) {
|
||||
console.log('computer woke up - fetching latest'); //eslint-disable-line no-console
|
||||
console.log('computer woke up - reconnecting'); //eslint-disable-line no-console
|
||||
reconnect();
|
||||
}
|
||||
lastTime.current = currentTime;
|
||||
@@ -92,12 +92,15 @@ function TeamController(props: Props) {
|
||||
props.markChannelAsReadOnFocus(props.currentChannelId);
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
if ((currentTime - blurTime.current) > UNREAD_CHECK_TIME_MILLISECONDS && props.currentTeamId) {
|
||||
if (props.graphQLEnabled) {
|
||||
props.fetchChannelsAndMembers(props.currentTeamId);
|
||||
} else {
|
||||
props.fetchMyChannelsAndMembersREST(props.currentTeamId);
|
||||
// Temporary flag to disable refetching of channel members on browser focus
|
||||
if (!props.disableRefetchingOnBrowserFocus) {
|
||||
const currentTime = Date.now();
|
||||
if ((currentTime - blurTime.current) > UNREAD_CHECK_TIME_MILLISECONDS && props.currentTeamId) {
|
||||
if (props.graphQLEnabled) {
|
||||
props.fetchChannelsAndMembers(props.currentTeamId);
|
||||
} else {
|
||||
props.fetchMyChannelsAndMembersREST(props.currentTeamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -895,6 +895,8 @@
|
||||
"admin.experimental.collapsedThreads.title": "Collapsed Reply Threads",
|
||||
"admin.experimental.defaultTheme.desc": "Set a default theme that applies to all new users on the system.",
|
||||
"admin.experimental.defaultTheme.title": "Default Theme:",
|
||||
"admin.experimental.disableRefetchingOnBrowserFocus.desc": "When true, Mattermost will not refetch channels and channel members when the browser regains focus. This may result in improved performance for users with many channels and channel members.",
|
||||
"admin.experimental.disableRefetchingOnBrowserFocus.title": "Disable data refetching on browser refocus:",
|
||||
"admin.experimental.emailBatchingBufferSize.desc": "Specify the maximum number of notifications batched into a single email.",
|
||||
"admin.experimental.emailBatchingBufferSize.example": "E.g.: \"256\"",
|
||||
"admin.experimental.emailBatchingBufferSize.title": "Email Batching Buffer Size:",
|
||||
|
||||
@@ -38,6 +38,7 @@ export type ClientConfig = {
|
||||
DefaultTheme: string;
|
||||
DiagnosticId: string;
|
||||
DiagnosticsEnabled: string;
|
||||
DisableRefetchingOnBrowserFocus: string;
|
||||
EmailLoginButtonBorderColor: string;
|
||||
EmailLoginButtonColor: string;
|
||||
EmailLoginButtonTextColor: string;
|
||||
@@ -526,7 +527,6 @@ export type EmailSettings = {
|
||||
LoginButtonColor: string;
|
||||
LoginButtonBorderColor: string;
|
||||
LoginButtonTextColor: string;
|
||||
EnableInactivityEmail: boolean;
|
||||
};
|
||||
|
||||
export type RateLimitSettings = {
|
||||
@@ -730,6 +730,7 @@ export type ExperimentalSettings = {
|
||||
EnableRemoteClusterService: boolean;
|
||||
EnableAppBar: boolean;
|
||||
PatchPluginsReactDOM: boolean;
|
||||
DisableRefetchingOnBrowserFocus: boolean;
|
||||
};
|
||||
|
||||
export type AnalyticsSettings = {
|
||||
|
||||