diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 44b59b7548..1596a327a4 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -4,15 +4,12 @@ on: workflows: ["Mattermost Build"] types: - completed + jobs: upload-s3: name: cd/Upload artifacts to S3 runs-on: ubuntu-22.04 - env: - REPO_NAME: ${{ github.event.repository.name }} - if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - name: cd/Configure AWS uses: aws-actions/configure-aws-credentials@07c2f971bac433df982ccc261983ae443861db49 # v1-node16 @@ -28,48 +25,46 @@ jobs: workflow_conclusion: success name: server-dist-artifact path: server/dist - # Get Branch name from calling workflow - # Search for the string "pull" and replace it with "PR" in branch-name - - name: cd/Get branch name - run: echo "BRANCH_NAME=$(echo ${{ github.event.workflow_run.head_branch }} | sed 's/^pull\//PR-/g')" >> $GITHUB_ENV - name: cd/Upload artifacts to S3 + env: + REPO_NAME: ${{ github.event.repository.name }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} run: | - aws s3 cp server/dist/ s3://pr-builds.mattermost.com/$REPO_NAME/$BRANCH_NAME/ --acl public-read --cache-control "no-cache" --recursive --no-progress - aws s3 cp server/dist/ s3://pr-builds.mattermost.com/$REPO_NAME/commit/${{ github.sha }}/ --acl public-read --cache-control "no-cache" --recursive --no-progress + aws s3 cp server/dist/ s3://pr-builds.mattermost.com/$REPO_NAME/commit/$COMMIT_SHA/ --acl public-read --cache-control "no-cache" --recursive --no-progress + build-docker: name: cd/Build and push docker image needs: upload-s3 - env: - REPO_NAME: ${{ github.event.repository.name }} runs-on: ubuntu-22.04 - if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - - name: cd/Login to Docker Hub - uses: docker/login-action@3da7dc6e2b31f99ef2cb9fb4c50fb0971e0d0139 # v2.1.0 - with: - username: ${{ secrets.DOCKERHUB_DEV_USERNAME }} - password: ${{ secrets.DOCKERHUB_DEV_TOKEN }} - - name: cd/Download artifacts - uses: dawidd6/action-download-artifact@0c49384d39ceb023b8040f480a25596fd6cf441b # v2.26.0 - with: - workflow: ${{ github.event.workflow_run.workflow_id }} - run_id: ${{ github.event.workflow_run.id }} - workflow_conclusion: success - name: server-build-artifact - path: server/build/ - - name: cd/Setup Docker Buildx - uses: docker/setup-buildx-action@11e8a2e2910826a92412015c515187a2d6750279 # v2.4 - - name: cd/Docker build and push - env: - DOCKER_CLI_EXPERIMENTAL: enabled - run: | - export TAG=$(echo "${{ github.event.pull_request.head.sha || github.sha }}" | cut -c1-7) - cd server/build - export DOCKER_CLI_EXPERIMENTAL=enabled - export MM_PACKAGE=https://pr-builds.mattermost.com/$REPO_NAME/commit/${{ github.sha }}/mattermost-team-linux-amd64.tar.gz - docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermostdevelopment/mm-te-test:${TAG} . + - name: cd/Login to Docker Hub + uses: docker/login-action@3da7dc6e2b31f99ef2cb9fb4c50fb0971e0d0139 # v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_DEV_USERNAME }} + password: ${{ secrets.DOCKERHUB_DEV_TOKEN }} + - name: cd/Download artifacts + uses: dawidd6/action-download-artifact@0c49384d39ceb023b8040f480a25596fd6cf441b # v2.26.0 + with: + workflow: ${{ github.event.workflow_run.workflow_id }} + run_id: ${{ github.event.workflow_run.id }} + workflow_conclusion: success + name: server-build-artifact + path: server/build/ + - name: cd/Setup Docker Buildx + uses: docker/setup-buildx-action@11e8a2e2910826a92412015c515187a2d6750279 # v2.4 + - name: cd/Docker build and push + env: + DOCKER_CLI_EXPERIMENTAL: enabled + REPO_NAME: ${{ github.event.repository.name }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + export TAG=$(echo "${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha }}" | cut -c1-7) + cd server/build + export DOCKER_CLI_EXPERIMENTAL=enabled + export MM_PACKAGE=https://pr-builds.mattermost.com/$REPO_NAME/commit/$COMMIT_SHA/mattermost-team-linux-amd64.tar.gz + docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermostdevelopment/mm-te-test:${TAG} . + # Temporary uploading also to mattermost/mm-te-test:${TAG} except mattermostdevelopment/mm-te-test:${TAG} # Context: https://community.mattermost.com/private-core/pl/3jzzxzfiji8hx833ewyuthzkjh build-docker-temp: @@ -77,40 +72,41 @@ jobs: needs: upload-s3 env: REPO_NAME: ${{ github.event.repository.name }} + runs-on: ubuntu-22.04 - if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - - name: cd/Login to Docker Hub - uses: docker/login-action@3da7dc6e2b31f99ef2cb9fb4c50fb0971e0d0139 # v2.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: cd/Download artifacts - uses: dawidd6/action-download-artifact@0c49384d39ceb023b8040f480a25596fd6cf441b # v2.26.0 - with: - workflow: ${{ github.event.workflow_run.workflow_id }} - run_id: ${{ github.event.workflow_run.id }} - workflow_conclusion: success - name: server-build-artifact - path: server/build/ - - name: cd/Setup Docker Buildx - uses: docker/setup-buildx-action@11e8a2e2910826a92412015c515187a2d6750279 # v2.4 - - name: cd/Docker build and push - env: - DOCKER_CLI_EXPERIMENTAL: enabled - run: | - export TAG=$(echo "${{ github.event.pull_request.head.sha || github.sha }}" | cut -c1-7) - cd server/build - export DOCKER_CLI_EXPERIMENTAL=enabled - export MM_PACKAGE=https://pr-builds.mattermost.com/$REPO_NAME/commit/${{ github.sha }}/mattermost-team-linux-amd64.tar.gz - docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermost/mm-te-test:${TAG} . + - name: cd/Login to Docker Hub + uses: docker/login-action@3da7dc6e2b31f99ef2cb9fb4c50fb0971e0d0139 # v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: cd/Download artifacts + uses: dawidd6/action-download-artifact@0c49384d39ceb023b8040f480a25596fd6cf441b # v2.26.0 + with: + workflow: ${{ github.event.workflow_run.workflow_id }} + run_id: ${{ github.event.workflow_run.id }} + workflow_conclusion: success + name: server-build-artifact + path: server/build/ + - name: cd/Setup Docker Buildx + uses: docker/setup-buildx-action@11e8a2e2910826a92412015c515187a2d6750279 # v2.4 + - name: cd/Docker build and push + env: + DOCKER_CLI_EXPERIMENTAL: enabled + REPO_NAME: ${{ github.event.repository.name }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + export TAG=$(echo "${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha }}" | cut -c1-7) + cd server/build + export DOCKER_CLI_EXPERIMENTAL=enabled + export MM_PACKAGE=https://pr-builds.mattermost.com/$REPO_NAME/commit/$COMMIT_SHA/mattermost-team-linux-amd64.tar.gz + docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermost/mm-te-test:${TAG} . + sentry: name: Send build info to sentry if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' + github.event.workflow_run.event == 'push' runs-on: ubuntu-22.04 env: SENTRY_AUTH_TOKEN: ${{ secrets.MM_SERVER_SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24ec4e83a1..44048c8641 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: run: if [[ -n $(git status --porcelain) ]]; then echo "Please update the serialized files using 'make gen-serialized'"; exit 1; fi check-mattermost-vet: name: Check style - runs-on: ubuntu-latest-8-cores + runs-on: ubuntu-22.04 defaults: run: working-directory: server diff --git a/e2e-tests/cypress/package-lock.json b/e2e-tests/cypress/package-lock.json index a639b718bd..ca9cde2d9a 100644 --- a/e2e-tests/cypress/package-lock.json +++ b/e2e-tests/cypress/package-lock.json @@ -12,7 +12,6 @@ "@babel/eslint-parser": "7.19.1", "@babel/eslint-plugin": "7.19.1", "@cypress/request": "2.88.11", - "@cypress/skip-test": "2.6.1", "@mattermost/types": "7.4.0", "@testing-library/cypress": "9.0.0", "@types/async": "3.2.16", @@ -2250,12 +2249,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/@cypress/skip-test": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@cypress/skip-test/-/skip-test-2.6.1.tgz", - "integrity": "sha512-X+ibefBiuOmC5gKG91wRIT0/OqXeETYvu7zXktjZ3yLeO186Y8ia0K7/gQUpAwuUi28DuqMd1+7tBQVtPkzbPA==", - "dev": true - }, "node_modules/@cypress/xvfb": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", @@ -19062,12 +19055,6 @@ } } }, - "@cypress/skip-test": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@cypress/skip-test/-/skip-test-2.6.1.tgz", - "integrity": "sha512-X+ibefBiuOmC5gKG91wRIT0/OqXeETYvu7zXktjZ3yLeO186Y8ia0K7/gQUpAwuUi28DuqMd1+7tBQVtPkzbPA==", - "dev": true - }, "@cypress/xvfb": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", diff --git a/e2e-tests/cypress/package.json b/e2e-tests/cypress/package.json index ada4a1ffec..a6c85ce248 100644 --- a/e2e-tests/cypress/package.json +++ b/e2e-tests/cypress/package.json @@ -3,7 +3,6 @@ "@babel/eslint-parser": "7.19.1", "@babel/eslint-plugin": "7.19.1", "@cypress/request": "2.88.11", - "@cypress/skip-test": "2.6.1", "@mattermost/types": "7.4.0", "@testing-library/cypress": "9.0.0", "@types/async": "3.2.16", diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_post_spec.js b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_post_spec.js index 4496aa9ff8..3ecf92ca94 100644 --- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_post_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_post_spec.js @@ -18,7 +18,6 @@ describe('Verify Accessibility Support in Post', () => { let otherUser; let testTeam; let testChannel; - let emojiPickerEnabled; before(() => { cy.apiInitSetup().then(({team, channel, user}) => { @@ -33,10 +32,6 @@ describe('Verify Accessibility Support in Post', () => { cy.apiAddUserToChannel(testChannel.id, otherUser.id); }); }); - - cy.apiGetConfig().then(({config}) => { - emojiPickerEnabled = config.ServiceSettings.EnableEmojiPicker; - }); }); }); @@ -179,17 +174,10 @@ describe('Verify Accessibility Support in Post', () => { cy.get(`#CENTER_time_${postId}`).should('be.focused'); cy.focused().tab(); - // eslint-disable-next-line no-negated-condition - if (!emojiPickerEnabled) { - // * Verify focus is on the actions button - cy.get(`#CENTER_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'more'); + for (let i = 0; i < 3; i++) { + // * Verify focus is on the reactions button + cy.get(`#recent_reaction_${i}`).should('have.class', 'emoticon--post-menu').and('have.attr', 'aria-label'); cy.focused().tab(); - } else { - for (let i = 0; i < 3; i++) { - // * Verify focus is on the reactions button - cy.get(`#recent_reaction_${i}`).should('have.class', 'emoticon--post-menu').and('have.attr', 'aria-label'); - cy.focused().tab(); - } } // * Verify focus is on the reactions button @@ -200,15 +188,17 @@ describe('Verify Accessibility Support in Post', () => { cy.get(`#CENTER_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save'); cy.focused().tab(); + // * Verify focus is on message actions button + cy.get(`#CENTER_actions_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'actions'); + cy.focused().tab(); + // * Verify focus is on the comment button cy.get(`#CENTER_commentIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'reply'); cy.focused().tab(); - if (emojiPickerEnabled) { - // * Verify focus is on the more button - cy.get(`#CENTER_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'More'); - cy.focused().tab(); - } + // * Verify focus is on the more button + cy.get(`#CENTER_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'more'); + cy.focused().tab(); // * Verify focus is on the post text cy.get(`#postMessageText_${postId}`).should('be.focused').and('have.attr', 'aria-readonly', 'true'); @@ -244,11 +234,13 @@ describe('Verify Accessibility Support in Post', () => { cy.get(`#rhsPostMessageText_${postId}`).should('be.focused').and('have.attr', 'aria-readonly', 'true'); cy.focused().tab({shift: true}); - if (emojiPickerEnabled) { - // * Verify focus is on the actions button - cy.get(`#RHS_COMMENT_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'More'); - cy.focused().tab({shift: true}); - } + // * Verify focus is on the more button + cy.get(`#RHS_COMMENT_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'more'); + cy.focused().tab({shift: true}); + + // * Verify focus is on message actions button + cy.get(`#RHS_COMMENT_actions_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'actions'); + cy.focused().tab({shift: true}); // * Verify focus is on the save icon cy.get(`#RHS_COMMENT_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save'); @@ -258,15 +250,9 @@ describe('Verify Accessibility Support in Post', () => { cy.get(`#RHS_COMMENT_reaction_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'add reaction'); cy.focused().tab({shift: true}); - // eslint-disable-next-line no-negated-condition - if (!emojiPickerEnabled) { - // * Verify focus is on the actions button - cy.get(`#RHS_COMMENT_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'more'); - cy.focused().tab({shift: true}); - } else { - cy.get('#recent_reaction_0').should('have.class', 'emoticon--post-menu').and('have.attr', 'aria-label'); - cy.focused().tab({shift: true}); - } + // * Verify focus is on most recent action + cy.get('#recent_reaction_0').should('have.class', 'emoticon--post-menu').and('have.attr', 'aria-label'); + cy.focused().tab({shift: true}); // * Verify focus is on the time cy.get(`#RHS_COMMENT_time_${postId}`).should('be.focused'); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js index 1880dafb8a..d62984cca0 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js @@ -15,7 +15,10 @@ import {getRandomId} from '../../../utils'; describe('Leave an archived channel', () => { let testTeam; let offTopicUrl; - + const channelType = { + public: 'Channel Type: Public', + archived: 'Channel Type: Archived', + }; before(() => { cy.apiUpdateConfig({ TeamSettings: { @@ -97,7 +100,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('#moreChannelsModal').should('be.visible').within(() => { // # Click on dropdown - cy.findByText('Show: Public Channels').should('be.visible').click(); + cy.findByText(channelType.public).should('be.visible').click(); // # Click archived channels cy.findByText('Archived Channels').click(); @@ -145,7 +148,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('.more-modal').should('be.visible').within(() => { // # Public channel list opens by default - cy.findByText('Show: Public Channels').should('be.visible').click(); + cy.findByText(channelType.public).should('be.visible').click(); // # Click on archived channels cy.findByText('Archived Channels').click(); @@ -198,7 +201,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('.more-modal').should('be.visible').within(() => { // # Public channels are shown by default - cy.findByText('Show: Public Channels').should('be.visible').click(); + cy.findByText(channelType.public).should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); @@ -252,7 +255,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('.more-modal').should('be.visible').within(() => { // # Show public channels is visible by default - cy.findByText('Show: Public Channels').should('be.visible').click(); + cy.findByText(channelType.public).should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); @@ -286,7 +289,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens and lands on public channels cy.get('#moreChannelsModal').should('be.visible').within(() => { - cy.findByText('Show: Public Channels').should('be.visible').click(); + cy.findByText(channelType.public).should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js index 95e31a75d4..d7fd550e37 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js @@ -14,6 +14,11 @@ import * as TIMEOUTS from '../../../fixtures/timeouts'; import {createPrivateChannel} from '../enterprise/elasticsearch_autocomplete/helpers'; +const channelType = { + public: 'Channel Type: Public', + archived: 'Channel Type: Archived', +}; + describe('Channels', () => { let testUser; let otherUser; @@ -65,7 +70,7 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Dropdown should be visible, defaulting to "Public Channels" - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').wait(TIMEOUTS.HALF_SEC); + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => { @@ -113,7 +118,7 @@ describe('Channels', () => { cy.findByText('Archived Channels').should('be.visible').click(); // * Channel test should be visible as an archived channel in the list - cy.wrap(el).should('contain', 'Show: Archived Channels'); + cy.wrap(el).should('contain', channelType.archived); }); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); @@ -196,7 +201,7 @@ describe('Channels', () => { // * Dropdown should be visible, defaulting to "Public Channels" cy.get('#channelsMoreDropdown').should('be.visible').within((el) => { - cy.wrap(el).should('contain', 'Show: Public Channels'); + cy.wrap(el).should('contain', channelType.public); }); // * Users should be able to type and search @@ -207,12 +212,12 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Users should be able to switch to "Archived Channels" list - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').click().within((el) => { + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).click().within((el) => { // # Click on archived channels item cy.findByText('Archived Channels').should('be.visible').click(); // * Modal should show the archived channels list - cy.wrap(el).should('contain', 'Show: Archived Channels'); + cy.wrap(el).should('contain', channelType.archived); }).wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').clear(); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2); @@ -250,7 +255,7 @@ function verifyMoreChannelsModal(isEnabled) { // * Verify that the more channels modal is open and with or without option to view archived channels cy.get('#moreChannelsModal').should('be.visible').within(() => { if (isEnabled) { - cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Show: Public Channels'); + cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', channelType.public); } else { cy.get('#channelsMoreDropdown').should('not.exist'); } diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/yearly_subscription_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/yearly_subscription_spec.js index 0eacc1350c..3c4b443381 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/yearly_subscription_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/yearly_subscription_spec.js @@ -142,7 +142,7 @@ describe('System Console - Subscriptions section', () => { cy.get('.RHS').find('button').should('be.enabled'); // # Change the user seats field to a value smaller than the current number of users - const lessThanUserCount = count - 5; + const lessThanUserCount = 1; cy.get('#input_UserSeats').clear().type(lessThanUserCount); // * Ensure that the yearly, monthly, and yearly saving prices match the new user seats value entered diff --git a/e2e-tests/cypress/tests/integration/channels/insights/insights_spec.ts b/e2e-tests/cypress/tests/integration/channels/insights/insights_spec.ts new file mode 100644 index 0000000000..9d3de5d6d1 --- /dev/null +++ b/e2e-tests/cypress/tests/integration/channels/insights/insights_spec.ts @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// *************************************************************** +// - [#] indicates a test step (e.g. # Go to a page) +// - [*] indicates an assertion (e.g. * Check the title) +// - Use element ID when selecting an element. Create one if none. +// *************************************************************** +// Stage: @prod + +describe('Insights', () => { + let teamA; + + before(() => { + cy.shouldHaveFeatureFlag('InsightsEnabled', true); + + cy.apiInitSetup().then(({team}) => { + teamA = team; + }); + }); + it('Check all the cards exist', () => { + cy.apiAdminLogin(); + + // # Go to the Insights view + cy.visit(`/${teamA.name}/activity-and-insights`); + + // * Check top channels exists + cy.get('.top-channels-card').should('exist'); + + // * Check top threads exists + cy.get('.top-threads-card').should('exist'); + + // * Check top boards exists because product mode is enabled + cy.get('.top-boards-card').should('exist'); + + // * Check top reactions exists + cy.get('.top-reactions-card').should('exist'); + + // * Check top dms exists + cy.get('.top-dms-card').should('exist'); + + // * Check least active channels exists + cy.get('.least-active-channels-card').should('exist'); + + // * Check top playbooks exists because product mode is enabled + cy.get('.top-playbooks-card').should('exist'); + }); +}); diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/app_bar_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/app_bar_spec.js index 3e201afde8..d24a0858cf 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/app_bar_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/app_bar_spec.js @@ -9,71 +9,45 @@ // Stage: @prod // Group: @playbooks -import {onlyOn} from '@cypress/skip-test'; - describe('channels > App Bar', {testIsolation: true}, () => { let testTeam; let testUser; - let testPlaybook; - let appBarEnabled; before(() => { cy.apiInitSetup().then(({team, user}) => { testTeam = team; testUser = user; - - // # Login as testUser - cy.apiLogin(testUser); - - // # Create a playbook - cy.apiCreateTestPlaybook({ - teamId: testTeam.id, - title: 'Playbook', - userId: testUser.id, - }).then((playbook) => { - testPlaybook = playbook; - - // # Start a playbook run - cy.apiRunPlaybook({ - teamId: testTeam.id, - playbookId: testPlaybook.id, - playbookRunName: 'Playbook Run', - ownerUserId: testUser.id, - }); - }); - - cy.apiGetConfig(true).then(({config}) => { - appBarEnabled = config.EnableAppBar === 'true'; - }); }); }); beforeEach(() => { - // # Size the viewport to show the RHS without covering posts. - cy.viewport('macbook-13'); - - // # Login as testUser - cy.apiLogin(testUser); + cy.apiAdminLogin(); }); describe('App Bar disabled', () => { it('should not show the Playbook App Bar icon', () => { - onlyOn(!appBarEnabled); + cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: false}}); + + // # Login as testUser + cy.apiLogin(testUser); // # Navigate directly to a non-playbook run channel cy.visit(`/${testTeam.name}/channels/town-square`); // * Verify App Bar icon is not showing - cy.get('#channel_view').within(() => { - cy.getPlaybooksAppBarIcon().should('not.exist'); - }); + cy.get('.app-bar').should('not.exist'); }); }); describe('App Bar enabled', () => { - it('should show the Playbook App Bar icon', () => { - onlyOn(appBarEnabled); + beforeEach(() => { + cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: true}}); + // # Login as testUser + cy.apiLogin(testUser); + }); + + it('should show the Playbook App Bar icon', () => { // # Navigate directly to a non-playbook run channel cy.visit(`/${testTeam.name}/channels/town-square`); @@ -82,8 +56,6 @@ describe('channels > App Bar', {testIsolation: true}, () => { }); it('should show "Playbooks" tooltip for Playbook App Bar icon', () => { - onlyOn(appBarEnabled); - // # Navigate directly to a non-playbook run channel cy.visit(`/${testTeam.name}/channels/town-square`); diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js index 546447ed0b..4b3abd6556 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js @@ -9,14 +9,11 @@ // Stage: @prod // Group: @playbooks -import {onlyOn} from '@cypress/skip-test'; - describe('channels > channel header', {testIsolation: true}, () => { let testTeam; let testUser; let testPlaybook; let testPlaybookRun; - let appBarEnabled; before(() => { cy.apiInitSetup().then(({team, user}) => { @@ -44,24 +41,16 @@ describe('channels > channel header', {testIsolation: true}, () => { testPlaybookRun = run; }); }); - - cy.apiGetConfig(true).then(({config}) => { - appBarEnabled = config.EnableAppBar === 'true'; - }); }); }); - beforeEach(() => { - // # Size the viewport to show the RHS without covering posts. - cy.viewport('macbook-13'); - - // # Login as testUser - cy.apiLogin(testUser); - }); - describe('App Bar enabled', () => { it('webapp should hide the Playbook channel header button', () => { - onlyOn(appBarEnabled); + cy.apiAdminLogin(); + cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: true}}); + + // # Login as testUser + cy.apiLogin(testUser); // # Navigate directly to a non-playbook run channel cy.visit(`/${testTeam.name}/channels/town-square`); @@ -74,9 +63,15 @@ describe('channels > channel header', {testIsolation: true}, () => { }); describe('App Bar disabled', () => { - it('webapp should show the Playbook channel header button', () => { - onlyOn(!appBarEnabled); + beforeEach(() => { + cy.apiAdminLogin(); + cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: false}}); + // # Login as testUser + cy.apiLogin(testUser); + }); + + it('webapp should show the Playbook channel header button', () => { // # Navigate directly to a non-playbook run channel cy.visit(`/${testTeam.name}/channels/town-square`); @@ -87,8 +82,6 @@ describe('channels > channel header', {testIsolation: true}, () => { }); it('tooltip text should show "Playbooks" for Playbook channel header button', () => { - onlyOn(!appBarEnabled); - // # Navigate directly to a non-playbook run channel cy.visit(`/${testTeam.name}/channels/town-square`); @@ -103,6 +96,11 @@ describe('channels > channel header', {testIsolation: true}, () => { }); describe('description text', () => { + beforeEach(() => { + // # Login as testUser + cy.apiLogin(testUser); + }); + it('should contain a link to the playbook', () => { // # Navigate directly to a playbook run channel cy.visit(`/${testTeam.name}/channels/playbook-run`); @@ -112,6 +110,7 @@ describe('channels > channel header', {testIsolation: true}, () => { expect(href).to.equals(`/playbooks/playbooks/${testPlaybook.id}`); }); }); + it('should contain a link to the overview page', () => { // # Navigate directly to a playbook run channel cy.visit(`/${testTeam.name}/channels/playbook-run`); diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/status_update_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/status_update_spec.js index 59f2a7811d..63bf6c8583 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/status_update_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/status_update_spec.js @@ -82,7 +82,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => { }); }); - it.skip('description link navigates to run overview', () => { + it('description link navigates to run overview', () => { // # Run the `/playbook update` slash command. cy.uiPostMessageQuickly('/playbook update'); diff --git a/e2e-tests/cypress/tests/integration/playbooks/lhs_spec.js b/e2e-tests/cypress/tests/integration/playbooks/lhs_spec.js index 63fa4d0bc4..1a74d96bf8 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/lhs_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/lhs_spec.js @@ -153,7 +153,7 @@ describe('lhs', {testIsolation: true}, () => { cy.findByTestId('dropdownmenu').should('be.visible'); }); - it.skip('can copy link', () => { + it('can copy link', () => { // # Visit the playbook run cy.visit(`/playbooks/runs/${playbookRun.id}`); stubClipboard().as('clipboard'); @@ -295,7 +295,7 @@ describe('lhs', {testIsolation: true}, () => { }); }); - it.skip('leave run, when on rdp of the same run', () => { + it('leave run, when on rdp of the same run', () => { // # Click on leave menu item getRunDropdownItemByText('Runs', playbookRun.name, 'Leave and unfollow run').click(); diff --git a/e2e-tests/cypress/tests/integration/playbooks/playbooks/edit_metrics_spec.js b/e2e-tests/cypress/tests/integration/playbooks/playbooks/edit_metrics_spec.js index 8f0c1feacd..0180c42a73 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/playbooks/edit_metrics_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/playbooks/edit_metrics_spec.js @@ -290,7 +290,7 @@ describe('playbooks > edit_metrics', {testIsolation: true}, () => { }); describe('delete metric', () => { - it.skip('verifies when clicking delete button; saved metrics have different confirmation text; deleted metrics are deleted', () => { + it('verifies when clicking delete button; saved metrics have different confirmation text; deleted metrics are deleted', () => { // # Visit the selected playbook cy.visit(`/playbooks/playbooks/${testPlaybook.id}`); diff --git a/e2e-tests/cypress/tests/integration/playbooks/runs/permissions_spec.js b/e2e-tests/cypress/tests/integration/playbooks/runs/permissions_spec.js index 4797d85fba..594487de28 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/runs/permissions_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/runs/permissions_spec.js @@ -160,9 +160,7 @@ describe('runs > permissions', {testIsolation: true}, () => { }); describe('should be visible', () => { - // XXX: Skipping this test, since public playbooks currently have no members. This will - // likely change in the future, so keeping the skeleton. - it.skip('to playbook members', () => { + it('to playbook members', () => { assertRunIsVisible(run, playbookMember); }); @@ -242,9 +240,7 @@ describe('runs > permissions', {testIsolation: true}, () => { }); describe('should be visible', () => { - // XXX: Skipping this test, since public playbooks currently have no members. This will - // likely change in the future. - it.skip('to playbook members', () => { + it('to playbook members', () => { assertRunIsVisible(run, playbookMember); }); @@ -332,10 +328,9 @@ describe('runs > permissions', {testIsolation: true}, () => { assertRunIsVisible(run, runParticipant); }); - // Skipping this test, since followers cannot follow a run with a private channel from - // a private playbook. (But leaving it for clarity in the code.) - it.skip('to run followers', () => { - assertRunIsVisible(run, runFollower); + // Followers cannot follow a run with a private channel from a private playbook + it('to run followers', () => { + assertRunIsNotVisible(run, runFollower); }); it('to admins in the team', () => { @@ -414,10 +409,9 @@ describe('runs > permissions', {testIsolation: true}, () => { assertRunIsVisible(run, runParticipant); }); - // Skipping this test, since followers cannot follow a run with a private channel from - // a private playbook. (But leaving it for clarity in the code.) - it.skip('to run followers', () => { - assertRunIsVisible(run, runFollower); + // Followers cannot follow a run with a private channel from a private playbook + it('to run followers', () => { + assertRunIsNotVisible(run, runFollower); }); it('to admins in the team', () => { diff --git a/e2e-tests/cypress/tests/integration/playbooks/runs/rdp_main_header_spec.js b/e2e-tests/cypress/tests/integration/playbooks/runs/rdp_main_header_spec.js index 4bb14ed9ad..e12771883b 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/runs/rdp_main_header_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/runs/rdp_main_header_spec.js @@ -692,7 +692,7 @@ describe('runs > run details page > header', {testIsolation: true}, () => { }); }); - describe.skip('Join action disabled', () => { + describe('Join action disabled', () => { beforeEach(() => { cy.apiLogin(testUser); diff --git a/e2e-tests/cypress/tests/integration/playbooks/runs/rdp_main_statusupdate_spec.js b/e2e-tests/cypress/tests/integration/playbooks/runs/rdp_main_statusupdate_spec.js index 973c7fab7d..fddc55378b 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/runs/rdp_main_statusupdate_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/runs/rdp_main_statusupdate_spec.js @@ -267,7 +267,7 @@ describe('runs > run details page > status update', {testIsolation: true}, () => }); }); - it.skip('requests an update and confirm', () => { + it('requests an update and confirm', () => { // # Click on request update cy.findByTestId('run-statusupdate-section'). should('be.visible'). @@ -281,11 +281,11 @@ describe('runs > run details page > status update', {testIsolation: true}, () => cy.visit(`${testTeam.name}/channels/${playbookRunChannelName}`); // * Assert that message has been sent - cy.getLastPost().contains(`${testUser.username} requested a status update for ${testPublicPlaybook.name}.`); + cy.getLastPost().contains(`${testViewerUser.username} requested a status update for ${testRun.name}.`); }); }); - it.skip('requests an update and cancel', () => { + it('requests an update and cancel', () => { // # Click request update cy.findByTestId('run-statusupdate-section'). should('be.visible'). diff --git a/e2e-tests/cypress/tests/support/api/role.js b/e2e-tests/cypress/tests/support/api/role.js index 6a8eed7082..d53862b72d 100644 --- a/e2e-tests/cypress/tests/support/api/role.js +++ b/e2e-tests/cypress/tests/support/api/role.js @@ -10,14 +10,14 @@ import xor from 'lodash.xor'; export const defaultRolesPermissions = { channel_admin: 'use_channel_mentions remove_reaction manage_public_channel_members use_group_mentions manage_channel_roles manage_private_channel_members add_reaction read_public_channel_groups create_post read_private_channel_groups', - channel_guest: 'upload_file edit_post create_post use_channel_mentions use_slash_commands read_channel add_reaction remove_reaction', - channel_user: 'manage_private_channel_members read_public_channel_groups delete_post read_private_channel_groups use_group_mentions manage_private_channel_properties delete_public_channel use_slash_commands add_reaction manage_public_channel_properties edit_post upload_file use_channel_mentions get_public_link read_channel delete_private_channel manage_public_channel_members create_post remove_reaction', + channel_guest: 'upload_file edit_post create_post use_channel_mentions read_channel add_reaction remove_reaction', + channel_user: 'manage_private_channel_members read_public_channel_groups delete_post read_private_channel_groups use_group_mentions manage_private_channel_properties delete_public_channel add_reaction manage_public_channel_properties edit_post upload_file use_channel_mentions get_public_link read_channel delete_private_channel manage_public_channel_members create_post remove_reaction', custom_group_user: '', playbook_admin: 'playbook_private_manage_properties playbook_public_make_private playbook_public_manage_members playbook_public_manage_roles playbook_public_manage_properties playbook_private_manage_members playbook_private_manage_roles', playbook_member: 'playbook_public_view playbook_public_manage_members playbook_public_manage_properties playbook_private_view playbook_private_manage_members playbook_private_manage_properties run_create', run_admin: 'run_manage_properties run_manage_members', run_member: 'run_view', - system_admin: 'sysconsole_write_environment_elasticsearch playbook_public_manage_properties sysconsole_write_authentication_ldap run_view manage_jobs manage_roles playbook_public_create manage_public_channel_properties sysconsole_read_plugins delete_post purge_elasticsearch_indexes sysconsole_read_integrations_bot_accounts read_data_retention_job manage_private_channel_members create_elasticsearch_post_indexing_job sysconsole_read_authentication_guest_access create_elasticsearch_post_aggregation_job join_public_teams sysconsole_read_site_public_links add_saml_idp_cert sysconsole_write_site_announcement_banner sysconsole_write_site_notices sysconsole_read_experimental_feature_flags sysconsole_read_site_users_and_teams manage_slash_commands sysconsole_read_authentication_ldap read_channel sysconsole_write_authentication_password list_users_without_team sysconsole_read_authentication_email add_saml_public_cert playbook_private_create promote_guest sysconsole_read_user_management_system_roles manage_public_channel_members create_data_retention_job add_saml_private_cert sysconsole_write_user_management_users sysconsole_read_compliance_compliance_monitoring playbook_public_manage_members sysconsole_write_environment_database sysconsole_write_user_management_teams playbook_private_manage_roles read_public_channel sysconsole_write_plugins sysconsole_read_authentication_openid sysconsole_write_user_management_groups sysconsole_write_site_file_sharing_and_downloads playbook_private_manage_properties sysconsole_read_site_customization join_public_channels add_user_to_team restore_custom_group download_compliance_export_result sysconsole_write_user_management_system_roles sysconsole_write_environment_session_lengths create_custom_group manage_private_channel_properties create_post_public remove_ldap_private_cert sysconsole_write_site_public_links import_team sysconsole_read_environment_developer sysconsole_read_environment_database sysconsole_read_environment_web_server use_channel_mentions view_team remove_others_reactions sysconsole_read_environment_session_lengths sysconsole_write_integrations_bot_accounts playbook_public_view use_group_mentions sysconsole_write_environment_web_server add_ldap_private_cert read_public_channel_groups invite_guest sysconsole_read_environment_smtp create_post sysconsole_read_about_edition_and_license sysconsole_read_authentication_signup sysconsole_read_authentication_saml sysconsole_read_environment_file_storage sysconsole_write_experimental_feature_flags sysconsole_write_site_localization sysconsole_write_environment_rate_limiting sysconsole_read_environment_rate_limiting sysconsole_read_products_boards get_saml_cert_status sysconsole_read_environment_high_availability manage_secure_connections read_compliance_export_job sysconsole_write_compliance_custom_terms_of_service read_user_access_token edit_post sysconsole_write_environment_logging sysconsole_read_environment_push_notification_server sysconsole_write_site_customization read_other_users_teams read_elasticsearch_post_aggregation_job sysconsole_write_compliance_data_retention_policy sysconsole_read_user_management_permissions sysconsole_read_site_emoji sysconsole_read_compliance_data_retention_policy read_license_information sysconsole_read_experimental_features read_deleted_posts sysconsole_read_environment_logging sysconsole_read_reporting_site_statistics test_elasticsearch sysconsole_read_site_posts add_reaction sysconsole_write_authentication_signup manage_outgoing_webhooks create_post_ephemeral sysconsole_read_environment_image_proxy invite_user manage_others_outgoing_webhooks create_user_access_token sysconsole_write_environment_image_proxy sysconsole_write_products_boards read_elasticsearch_post_indexing_job purge_bleve_indexes sysconsole_write_environment_performance_monitoring sysconsole_write_authentication_guest_access sysconsole_read_compliance_custom_terms_of_service edit_others_posts sysconsole_write_billing get_saml_metadata_from_idp sysconsole_write_authentication_saml create_post_bleve_indexes_job invalidate_caches sysconsole_write_experimental_bleve view_members manage_others_bots run_create join_private_teams convert_private_channel_to_public read_audits assign_bot read_jobs remove_user_from_team revoke_user_access_token manage_team sysconsole_read_reporting_server_logs get_public_link manage_others_slash_commands manage_system delete_public_channel read_private_channel_groups sysconsole_read_authentication_mfa delete_emojis list_private_teams create_emojis sysconsole_read_billing sysconsole_write_site_emoji invalidate_email_invite sysconsole_write_environment_file_storage sysconsole_write_compliance_compliance_monitoring remove_saml_public_cert sysconsole_read_compliance_compliance_export sysconsole_read_site_localization use_slash_commands manage_team_roles list_public_teams get_logs sysconsole_write_integrations_integration_management sysconsole_read_integrations_cors manage_oauth delete_others_emojis sysconsole_write_integrations_gif manage_incoming_webhooks sysconsole_write_authentication_email create_private_channel playbook_private_make_public manage_bots add_ldap_public_cert remove_ldap_public_cert sysconsole_write_site_notifications sysconsole_write_environment_developer playbook_private_manage_members sysconsole_read_user_management_teams edit_custom_group remove_reaction playbook_public_manage_roles sysconsole_write_reporting_server_logs read_others_bots sysconsole_write_site_posts sysconsole_read_site_notifications sysconsole_read_authentication_password playbook_private_view manage_system_wide_oauth get_analytics list_team_channels sysconsole_write_user_management_channels delete_private_channel manage_custom_group_members test_s3 create_ldap_sync_job sysconsole_read_integrations_integration_management test_site_url recycle_database_connections sysconsole_read_site_announcement_banner test_email manage_shared_channels read_bots sysconsole_write_environment_smtp sysconsole_read_experimental_bleve sysconsole_write_environment_push_notification_server sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_write_reporting_site_statistics sysconsole_write_site_users_and_teams demote_to_guest create_team test_ldap remove_saml_idp_cert delete_others_posts edit_other_users sysconsole_write_reporting_team_statistics sysconsole_read_integrations_gif sysconsole_read_site_notices sysconsole_write_about_edition_and_license manage_others_incoming_webhooks run_manage_members create_bot sysconsole_write_authentication_mfa sysconsole_read_user_management_users assign_system_admin_role sysconsole_write_experimental_features edit_brand create_group_channel sysconsole_write_authentication_openid create_direct_channel manage_license_information reload_config manage_channel_roles sysconsole_read_user_management_groups create_compliance_export_job read_ldap_sync_job upload_file sysconsole_read_site_file_sharing_and_downloads delete_custom_group sysconsole_read_user_management_channels sysconsole_write_compliance_compliance_export remove_saml_private_cert sysconsole_read_environment_performance_monitoring create_public_channel sysconsole_write_integrations_cors sysconsole_write_environment_high_availability playbook_public_make_private run_manage_properties sysconsole_read_reporting_team_statistics convert_public_channel_to_private', + system_admin: 'sysconsole_write_environment_elasticsearch playbook_public_manage_properties sysconsole_write_authentication_ldap run_view manage_jobs manage_roles playbook_public_create manage_public_channel_properties sysconsole_read_plugins delete_post purge_elasticsearch_indexes sysconsole_read_integrations_bot_accounts read_data_retention_job manage_private_channel_members create_elasticsearch_post_indexing_job sysconsole_read_authentication_guest_access create_elasticsearch_post_aggregation_job join_public_teams sysconsole_read_site_public_links add_saml_idp_cert sysconsole_write_site_announcement_banner sysconsole_write_site_notices sysconsole_read_experimental_feature_flags sysconsole_read_site_users_and_teams manage_slash_commands sysconsole_read_authentication_ldap read_channel sysconsole_write_authentication_password list_users_without_team sysconsole_read_authentication_email add_saml_public_cert playbook_private_create promote_guest sysconsole_read_user_management_system_roles manage_public_channel_members create_data_retention_job add_saml_private_cert sysconsole_write_user_management_users sysconsole_read_compliance_compliance_monitoring playbook_public_manage_members sysconsole_write_environment_database sysconsole_write_user_management_teams playbook_private_manage_roles read_public_channel sysconsole_write_plugins sysconsole_read_authentication_openid sysconsole_write_user_management_groups sysconsole_write_site_file_sharing_and_downloads playbook_private_manage_properties sysconsole_read_site_customization join_public_channels add_user_to_team restore_custom_group download_compliance_export_result sysconsole_write_user_management_system_roles sysconsole_write_environment_session_lengths create_custom_group manage_private_channel_properties create_post_public remove_ldap_private_cert sysconsole_write_site_public_links import_team sysconsole_read_environment_developer sysconsole_read_environment_database sysconsole_read_environment_web_server use_channel_mentions view_team remove_others_reactions sysconsole_read_environment_session_lengths sysconsole_write_integrations_bot_accounts playbook_public_view use_group_mentions sysconsole_write_environment_web_server add_ldap_private_cert read_public_channel_groups invite_guest sysconsole_read_environment_smtp create_post sysconsole_read_about_edition_and_license sysconsole_read_authentication_signup sysconsole_read_authentication_saml sysconsole_read_environment_file_storage sysconsole_write_experimental_feature_flags sysconsole_write_site_localization sysconsole_write_environment_rate_limiting sysconsole_read_environment_rate_limiting sysconsole_read_products_boards get_saml_cert_status sysconsole_read_environment_high_availability manage_secure_connections read_compliance_export_job sysconsole_write_compliance_custom_terms_of_service read_user_access_token edit_post sysconsole_write_environment_logging sysconsole_read_environment_push_notification_server sysconsole_write_site_customization read_other_users_teams read_elasticsearch_post_aggregation_job sysconsole_write_compliance_data_retention_policy sysconsole_read_user_management_permissions sysconsole_read_site_emoji sysconsole_read_compliance_data_retention_policy read_license_information sysconsole_read_experimental_features read_deleted_posts sysconsole_read_environment_logging sysconsole_read_reporting_site_statistics test_elasticsearch sysconsole_read_site_posts add_reaction sysconsole_write_authentication_signup manage_outgoing_webhooks create_post_ephemeral sysconsole_read_environment_image_proxy invite_user manage_others_outgoing_webhooks create_user_access_token sysconsole_write_environment_image_proxy sysconsole_write_products_boards read_elasticsearch_post_indexing_job purge_bleve_indexes sysconsole_write_environment_performance_monitoring sysconsole_write_authentication_guest_access sysconsole_read_compliance_custom_terms_of_service edit_others_posts sysconsole_write_billing get_saml_metadata_from_idp sysconsole_write_authentication_saml create_post_bleve_indexes_job invalidate_caches sysconsole_write_experimental_bleve view_members manage_others_bots run_create join_private_teams convert_private_channel_to_public read_audits assign_bot read_jobs remove_user_from_team revoke_user_access_token manage_team sysconsole_read_reporting_server_logs get_public_link manage_others_slash_commands manage_system delete_public_channel read_private_channel_groups sysconsole_read_authentication_mfa delete_emojis list_private_teams create_emojis sysconsole_read_billing sysconsole_write_site_emoji invalidate_email_invite sysconsole_write_environment_file_storage sysconsole_write_compliance_compliance_monitoring remove_saml_public_cert sysconsole_read_compliance_compliance_export sysconsole_read_site_localization manage_team_roles list_public_teams get_logs sysconsole_write_integrations_integration_management sysconsole_read_integrations_cors manage_oauth delete_others_emojis sysconsole_write_integrations_gif manage_incoming_webhooks sysconsole_write_authentication_email create_private_channel playbook_private_make_public manage_bots add_ldap_public_cert remove_ldap_public_cert sysconsole_write_site_notifications sysconsole_write_environment_developer playbook_private_manage_members sysconsole_read_user_management_teams edit_custom_group remove_reaction playbook_public_manage_roles sysconsole_write_reporting_server_logs read_others_bots sysconsole_write_site_posts sysconsole_read_site_notifications sysconsole_read_authentication_password playbook_private_view manage_system_wide_oauth get_analytics list_team_channels sysconsole_write_user_management_channels delete_private_channel manage_custom_group_members test_s3 create_ldap_sync_job sysconsole_read_integrations_integration_management test_site_url recycle_database_connections sysconsole_read_site_announcement_banner test_email manage_shared_channels read_bots sysconsole_write_environment_smtp sysconsole_read_experimental_bleve sysconsole_write_environment_push_notification_server sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_write_reporting_site_statistics sysconsole_write_site_users_and_teams demote_to_guest create_team test_ldap remove_saml_idp_cert delete_others_posts edit_other_users sysconsole_write_reporting_team_statistics sysconsole_read_integrations_gif sysconsole_read_site_notices sysconsole_write_about_edition_and_license manage_others_incoming_webhooks run_manage_members create_bot sysconsole_write_authentication_mfa sysconsole_read_user_management_users assign_system_admin_role sysconsole_write_experimental_features edit_brand create_group_channel sysconsole_write_authentication_openid create_direct_channel manage_license_information reload_config manage_channel_roles sysconsole_read_user_management_groups create_compliance_export_job read_ldap_sync_job upload_file sysconsole_read_site_file_sharing_and_downloads delete_custom_group sysconsole_read_user_management_channels sysconsole_write_compliance_compliance_export remove_saml_private_cert sysconsole_read_environment_performance_monitoring create_public_channel sysconsole_write_integrations_cors sysconsole_write_environment_high_availability playbook_public_make_private run_manage_properties sysconsole_read_reporting_team_statistics convert_public_channel_to_private', system_custom_group_admin: 'create_custom_group edit_custom_group delete_custom_group restore_custom_group manage_custom_group_members', system_guest: 'create_group_channel create_direct_channel', system_manager: ' sysconsole_read_site_announcement_banner manage_private_channel_properties edit_brand read_private_channel_groups manage_private_channel_members manage_team_roles sysconsole_write_environment_session_lengths sysconsole_read_site_emoji sysconsole_write_environment_developer sysconsole_read_user_management_groups sysconsole_write_user_management_groups sysconsole_write_environment_rate_limiting delete_private_channel sysconsole_read_environment_performance_monitoring sysconsole_read_environment_rate_limiting sysconsole_write_user_management_teams sysconsole_write_integrations_integration_management sysconsole_write_site_public_links sysconsole_read_authentication_ldap sysconsole_write_integrations_cors reload_config sysconsole_write_user_management_channels sysconsole_read_environment_high_availability sysconsole_read_site_users_and_teams sysconsole_read_user_management_teams sysconsole_write_site_users_and_teams sysconsole_read_site_customization sysconsole_write_environment_high_availability sysconsole_read_integrations_bot_accounts sysconsole_read_authentication_guest_access sysconsole_read_site_public_links read_elasticsearch_post_indexing_job sysconsole_read_user_management_channels sysconsole_read_reporting_team_statistics invalidate_caches sysconsole_read_authentication_signup read_elasticsearch_post_aggregation_job sysconsole_write_environment_smtp manage_public_channel_members list_public_teams add_user_to_team sysconsole_read_environment_web_server sysconsole_read_site_localization get_logs sysconsole_write_site_posts sysconsole_write_integrations_bot_accounts sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_read_environment_smtp list_private_teams read_public_channel_groups sysconsole_write_environment_file_storage sysconsole_write_integrations_gif manage_public_channel_properties sysconsole_write_environment_performance_monitoring sysconsole_write_site_notifications sysconsole_read_site_notifications sysconsole_read_environment_image_proxy sysconsole_write_site_announcement_banner sysconsole_write_site_emoji test_site_url sysconsole_read_integrations_gif sysconsole_write_environment_logging convert_public_channel_to_private get_analytics sysconsole_read_user_management_permissions sysconsole_write_environment_image_proxy test_elasticsearch recycle_database_connections sysconsole_write_site_localization sysconsole_read_reporting_server_logs create_elasticsearch_post_indexing_job sysconsole_read_reporting_site_statistics test_ldap delete_public_channel sysconsole_write_environment_push_notification_server read_license_information sysconsole_write_products_boards sysconsole_read_about_edition_and_license convert_private_channel_to_public sysconsole_read_integrations_integration_management create_elasticsearch_post_aggregation_job purge_elasticsearch_indexes sysconsole_read_environment_database join_public_teams sysconsole_read_authentication_email sysconsole_read_environment_push_notification_server view_team read_channel sysconsole_read_authentication_password read_ldap_sync_job sysconsole_read_integrations_cors sysconsole_read_environment_logging manage_team sysconsole_read_authentication_openid read_public_channel sysconsole_write_environment_elasticsearch sysconsole_read_plugins manage_channel_roles remove_user_from_team test_email sysconsole_write_site_file_sharing_and_downloads test_s3 sysconsole_read_site_file_sharing_and_downloads sysconsole_read_site_notices sysconsole_read_environment_file_storage join_private_teams sysconsole_read_products_boards sysconsole_read_environment_session_lengths sysconsole_write_environment_database sysconsole_read_authentication_saml sysconsole_read_authentication_mfa sysconsole_write_site_notices sysconsole_write_environment_web_server sysconsole_read_site_posts sysconsole_read_environment_developer sysconsole_write_site_customization', diff --git a/server/Makefile b/server/Makefile index 143d6e7cc9..77f6941869 100644 --- a/server/Makefile +++ b/server/Makefile @@ -153,7 +153,7 @@ PLUGIN_PACKAGES += mattermost-plugin-nps-v1.3.1 PLUGIN_PACKAGES += mattermost-plugin-todo-v0.6.1 PLUGIN_PACKAGES += mattermost-plugin-welcomebot-v1.2.0 PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.0 -PLUGIN_PACKAGES += mattermost-plugin-apps-v1.2.0 +PLUGIN_PACKAGES += mattermost-plugin-apps-v1.2.1 # Prepares the enterprise build if exists. The IGNORE stuff is a hack to get the Makefile to execute the commands outside a target ifeq ($(BUILD_ENTERPRISE_READY),true) @@ -292,7 +292,7 @@ endif golangci-lint: ## Run golangci-lint on codebase @# Keep the version in sync with the command in .circleci/config.yml - $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1 + $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2 @echo Running golangci-lint $(GOBIN)/golangci-lint run ./... diff --git a/server/boards/api/blocks.go b/server/boards/api/blocks.go index 400ff624c1..49add99fef 100644 --- a/server/boards/api/blocks.go +++ b/server/boards/api/blocks.go @@ -304,7 +304,7 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { // this query param exists when creating template from board, or board from template sourceBoardID := r.URL.Query().Get("sourceBoardID") if sourceBoardID != "" { - if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, blocks); updateFileIDsErr != nil { + if updateFileIDsErr := a.app.CopyAndUpdateCardFiles(sourceBoardID, userID, blocks, false); updateFileIDsErr != nil { a.errorResponse(w, r, updateFileIDsErr) return } diff --git a/server/boards/api/files.go b/server/boards/api/files.go index 2ca2102863..482fa0a622 100644 --- a/server/boards/api/files.go +++ b/server/boards/api/files.go @@ -312,7 +312,7 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("teamID", board.TeamID) auditRec.AddMeta("filename", handle.Filename) - fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename) + fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename, board.IsTemplate) if err != nil { a.errorResponse(w, r, err) return diff --git a/server/boards/app/blocks.go b/server/boards/app/blocks.go index 41de8d5736..136bf736fe 100644 --- a/server/boards/app/blocks.go +++ b/server/boards/app/blocks.go @@ -7,11 +7,9 @@ import ( "errors" "fmt" "path/filepath" - "strings" "github.com/mattermost/mattermost-server/server/v8/boards/model" "github.com/mattermost/mattermost-server/server/v8/boards/services/notify" - "github.com/mattermost/mattermost-server/server/v8/boards/utils" "github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog" ) @@ -39,6 +37,11 @@ func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTe return nil, err } + err = a.CopyAndUpdateCardFiles(boardID, userID, blocks, asTemplate) + if err != nil { + return nil, err + } + a.blockChangeNotifier.Enqueue(func() error { for _, block := range blocks { a.wsAdapter.BroadcastBlockChange(board.TeamID, block) @@ -286,95 +289,6 @@ func (a *App) InsertBlocksAndNotify(blocks []*model.Block, modifiedByID string, return blocks, nil } -func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block) error { - // Images attached in cards have a path comprising the card's board ID. - // When we create a template from this board, we need to copy the files - // with the new board ID in path. - // Not doing so causing images in templates (and boards created from this - // template) to fail to load. - - // look up ID of source sourceBoard, which may be different than the blocks. - sourceBoard, err := a.GetBoard(sourceBoardID) - if err != nil || sourceBoard == nil { - return fmt.Errorf("cannot fetch source board %s for CopyCardFiles: %w", sourceBoardID, err) - } - - var destTeamID string - var destBoardID string - - for i := range copiedBlocks { - block := copiedBlocks[i] - fileName := "" - isOk := false - - switch block.Type { - case model.TypeImage: - fileName, isOk = block.Fields["fileId"].(string) - if !isOk || fileName == "" { - continue - } - case model.TypeAttachment: - fileName, isOk = block.Fields["attachmentId"].(string) - if !isOk || fileName == "" { - continue - } - default: - continue - } - - // create unique filename in case we are copying cards within the same board. - ext := filepath.Ext(fileName) - destFilename := utils.NewID(utils.IDTypeNone) + ext - - if destBoardID == "" || block.BoardID != destBoardID { - destBoardID = block.BoardID - destBoard, err := a.GetBoard(destBoardID) - if err != nil { - return fmt.Errorf("cannot fetch destination board %s for CopyCardFiles: %w", sourceBoardID, err) - } - destTeamID = destBoard.TeamID - } - - sourceFilePath := filepath.Join(sourceBoard.TeamID, sourceBoard.ID, fileName) - destinationFilePath := filepath.Join(destTeamID, block.BoardID, destFilename) - - a.logger.Debug( - "Copying card file", - mlog.String("sourceFilePath", sourceFilePath), - mlog.String("destinationFilePath", destinationFilePath), - ) - - if err := a.filesBackend.CopyFile(sourceFilePath, destinationFilePath); err != nil { - a.logger.Error( - "CopyCardFiles failed to copy file", - mlog.String("sourceFilePath", sourceFilePath), - mlog.String("destinationFilePath", destinationFilePath), - mlog.Err(err), - ) - } - if block.Type == model.TypeAttachment { - block.Fields["attachmentId"] = destFilename - parts := strings.Split(fileName, ".") - fileInfoID := parts[0][1:] - fileInfo, err := a.store.GetFileInfo(fileInfoID) - if err != nil { - return fmt.Errorf("CopyCardFiles: cannot retrieve original fileinfo: %w", err) - } - newParts := strings.Split(destFilename, ".") - newFileID := newParts[0][1:] - fileInfo.Id = newFileID - err = a.store.SaveFileInfo(fileInfo) - if err != nil { - return fmt.Errorf("CopyCardFiles: cannot create fileinfo: %w", err) - } - } else { - block.Fields["fileId"] = destFilename - } - } - - return nil -} - func (a *App) GetBlockByID(blockID string) (*model.Block, error) { return a.store.GetBlock(blockID) } diff --git a/server/boards/app/boards.go b/server/boards/app/boards.go index ff81e81bff..1f7fc8c026 100644 --- a/server/boards/app/boards.go +++ b/server/boards/app/boards.go @@ -184,8 +184,13 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (* } // copy any file attachments from the duplicated blocks. - if err = a.CopyCardFiles(boardID, bab.Blocks); err != nil { - a.logger.Error("Could not copy files while duplicating board", mlog.String("BoardID", boardID), mlog.Err(err)) + err = a.CopyAndUpdateCardFiles(boardID, userID, bab.Blocks, asTemplate) + if err != nil { + dbab := model.NewDeleteBoardsAndBlocksFromBabs(bab) + if err = a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil { + a.logger.Error("Cannot delete board after duplication error when updating block's file info", mlog.String("boardID", bab.Boards[0].ID), mlog.Err(err)) + } + return nil, nil, fmt.Errorf("could not patch file IDs while duplicating board %s: %w", boardID, err) } if !asTemplate { @@ -196,44 +201,6 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (* } } - // bab.Blocks now has updated file ids for any blocks containing files. We need to store them. - blockIDs := make([]string, 0) - blockPatches := make([]model.BlockPatch, 0) - - for _, block := range bab.Blocks { - fieldName := "" - if block.Type == model.TypeImage { - fieldName = "fileId" - } else if block.Type == model.TypeAttachment { - fieldName = "attachmentId" - } - if fieldName != "" { - if fieldID, ok := block.Fields[fieldName]; ok { - blockIDs = append(blockIDs, block.ID) - blockPatches = append(blockPatches, model.BlockPatch{ - UpdatedFields: map[string]interface{}{ - fieldName: fieldID, - }, - }) - } - } - } - a.logger.Debug("Duplicate boards patching file IDs", mlog.Int("count", len(blockIDs))) - - if len(blockIDs) != 0 { - patches := &model.BlockPatchBatch{ - BlockIDs: blockIDs, - BlockPatches: blockPatches, - } - if err = a.store.PatchBlocks(patches, userID); err != nil { - dbab := model.NewDeleteBoardsAndBlocksFromBabs(bab) - if err = a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil { - a.logger.Error("Cannot delete board after duplication error when updating block's file info", mlog.String("boardID", bab.Boards[0].ID), mlog.Err(err)) - } - return nil, nil, fmt.Errorf("could not patch file IDs while duplicating board %s: %w", boardID, err) - } - } - a.blockChangeNotifier.Enqueue(func() error { teamID := "" for _, board := range bab.Boards { diff --git a/server/boards/app/export.go b/server/boards/app/export.go index 88cc95ab86..d8a03d78a8 100644 --- a/server/boards/app/export.go +++ b/server/boards/app/export.go @@ -95,10 +95,10 @@ func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.Exp if err = a.writeArchiveBlockLine(w, block); err != nil { return err } - if block.Type == model.TypeImage { - filename, err2 := extractImageFilename(block) + if block.Type == model.TypeImage || block.Type == model.TypeAttachment { + filename, err2 := extractFilename(block) if err2 != nil { - return err + return err2 } files = append(files, filename) } @@ -208,7 +208,10 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, return err } - src, err := a.GetFileReader(opt.TeamID, boardID, filename) + _, fileReader, err := a.GetFile(opt.TeamID, boardID, filename) + if err != nil && !model.IsErrNotFound(err) { + return err + } if err != nil { // just log this; image file is missing but we'll still export an equivalent board a.logger.Error("image file missing for export", @@ -218,9 +221,9 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, ) return nil } - defer src.Close() + defer fileReader.Close() - _, err = io.Copy(dest, src) + _, err = io.Copy(dest, fileReader) return err } @@ -239,10 +242,13 @@ func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) { return boards, nil } -func extractImageFilename(imageBlock *model.Block) (string, error) { - f, ok := imageBlock.Fields["fileId"] +func extractFilename(block *model.Block) (string, error) { + f, ok := block.Fields["fileId"] if !ok { - return "", model.ErrInvalidImageBlock + f, ok = block.Fields["attachmentId"] + if !ok { + return "", model.ErrInvalidImageBlock + } } filename, ok := f.(string) diff --git a/server/boards/app/files.go b/server/boards/app/files.go index 9ca07ef323..0212a5176f 100644 --- a/server/boards/app/files.go +++ b/server/boards/app/files.go @@ -18,12 +18,10 @@ import ( "github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog" ) -const emptyString = "empty" - var errEmptyFilename = errors.New("IsFileArchived: empty filename not allowed") var ErrFileNotFound = errors.New("file not found") -func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (string, error) { +func (a *App) SaveFile(reader io.Reader, teamID, boardID, filename string, asTemplate bool) (string, error) { // NOTE: File extension includes the dot fileExtension := strings.ToLower(filepath.Ext(filename)) if fileExtension == ".jpeg" { @@ -31,44 +29,26 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin } createdFilename := utils.NewID(utils.IDTypeNone) - fullFilename := fmt.Sprintf(`%s%s`, createdFilename, fileExtension) - filePath := filepath.Join(utils.GetBaseFilePath(), fullFilename) + newFileName := fmt.Sprintf(`%s%s`, createdFilename, fileExtension) + if asTemplate { + newFileName = filename + } + filePath := getDestinationFilePath(asTemplate, teamID, boardID, newFileName) fileSize, appErr := a.filesBackend.WriteFile(reader, filePath) if appErr != nil { return "", fmt.Errorf("unable to store the file in the files storage: %w", appErr) } - now := utils.GetMillis() - - fileInfo := &mm_model.FileInfo{ - Id: createdFilename[1:], - CreatorId: "boards", - PostId: emptyString, - ChannelId: emptyString, - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Path: filePath, - ThumbnailPath: emptyString, - PreviewPath: emptyString, - Name: filename, - Extension: fileExtension, - Size: fileSize, - MimeType: emptyString, - Width: 0, - Height: 0, - HasPreviewImage: false, - MiniPreview: nil, - Content: "", - RemoteId: nil, - } + fileInfo := model.NewFileInfo(filename) + fileInfo.Id = getFileInfoID(createdFilename) + fileInfo.Path = filePath + fileInfo.Size = fileSize err := a.store.SaveFileInfo(fileInfo) if err != nil { return "", err } - - return fullFilename, nil + return newFileName, nil } func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, error) { @@ -79,8 +59,7 @@ func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, error) { // filename is in the format 7. // we want to extract the part of this as this // will be the fileinfo id. - parts := strings.Split(filename, ".") - fileInfoID := parts[0][1:] + fileInfoID := getFileInfoID(strings.Split(filename, ".")[0]) fileInfo, err := a.store.GetFileInfo(fileInfoID) if err != nil { return nil, err @@ -90,10 +69,33 @@ func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, error) { } func (a *App) GetFile(teamID, rootID, fileName string) (*mm_model.FileInfo, filestore.ReadCloseSeeker, error) { + fileInfo, filePath, err := a.GetFilePath(teamID, rootID, fileName) + if err != nil { + a.logger.Error("GetFile: Failed to GetFilePath.", mlog.String("Team", teamID), mlog.String("board", rootID), mlog.String("filename", fileName), mlog.Err(err)) + return nil, nil, err + } + + exists, err := a.filesBackend.FileExists(filePath) + if err != nil { + a.logger.Error("GetFile: Failed to check if file exists as path. ", mlog.String("Path", filePath), mlog.Err(err)) + return nil, nil, err + } + if !exists { + return nil, nil, ErrFileNotFound + } + + reader, err := a.filesBackend.Reader(filePath) + if err != nil { + a.logger.Error("GetFile: Failed to get file reader of existing file at path", mlog.String("Path", filePath), mlog.Err(err)) + return nil, nil, err + } + return fileInfo, reader, nil +} + +func (a *App) GetFilePath(teamID, rootID, fileName string) (*mm_model.FileInfo, string, error) { fileInfo, err := a.GetFileInfo(fileName) if err != nil && !model.IsErrNotFound(err) { - a.logger.Error("111") - return nil, nil, err + return nil, "", err } var filePath string @@ -104,22 +106,23 @@ func (a *App) GetFile(teamID, rootID, fileName string) (*mm_model.FileInfo, file filePath = filepath.Join(teamID, rootID, fileName) } - exists, err := a.filesBackend.FileExists(filePath) - if err != nil { - a.logger.Error(fmt.Sprintf("GetFile: Failed to check if file exists as path. Path: %s, error: %e", filePath, err)) - return nil, nil, err - } + return fileInfo, filePath, nil +} - if !exists { - return nil, nil, ErrFileNotFound +func getDestinationFilePath(isTemplate bool, teamID, boardID, filename string) string { + // if saving a file for a template, save using the "old method" that is /teamID/boardID/fileName + // this will prevent template files from being deleted by DataRetention, + // which deletes all files inside the "date" subdirectory + if isTemplate { + return filepath.Join(teamID, boardID, filename) } + return filepath.Join(utils.GetBaseFilePath(), filename) +} - reader, err := a.filesBackend.Reader(filePath) - if err != nil { - a.logger.Error(fmt.Sprintf("GetFile: Failed to get file reader of existing file at path: %s, error: %e", filePath, err)) - return nil, nil, err - } - return fileInfo, reader, nil +func getFileInfoID(fileName string) string { + // Boards ids are 27 characters long with a prefix character. + // removing the prefix, returns the 26 character uuid + return fileName[1:] } func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) { @@ -175,3 +178,121 @@ func (a *App) MoveFile(channelID, teamID, boardID, filename string) error { } return nil } + +func (a *App) CopyAndUpdateCardFiles(boardID, userID string, blocks []*model.Block, asTemplate bool) error { + newFileNames, err := a.CopyCardFiles(boardID, blocks, asTemplate) + if err != nil { + a.logger.Error("Could not copy files while duplicating board", mlog.String("BoardID", boardID), mlog.Err(err)) + } + + // blocks now has updated file ids for any blocks containing files. We need to update the database for them. + blockIDs := make([]string, 0) + blockPatches := make([]model.BlockPatch, 0) + for _, block := range blocks { + if block.Type == model.TypeImage || block.Type == model.TypeAttachment { + if fileID, ok := block.Fields["fileId"].(string); ok { + blockIDs = append(blockIDs, block.ID) + blockPatches = append(blockPatches, model.BlockPatch{ + UpdatedFields: map[string]interface{}{ + "fileId": newFileNames[fileID], + }, + DeletedFields: []string{"attachmentId"}, + }) + } + } + } + a.logger.Debug("Duplicate boards patching file IDs", mlog.Int("count", len(blockIDs))) + + if len(blockIDs) != 0 { + patches := &model.BlockPatchBatch{ + BlockIDs: blockIDs, + BlockPatches: blockPatches, + } + if err := a.store.PatchBlocks(patches, userID); err != nil { + return fmt.Errorf("could not patch file IDs while duplicating board %s: %w", boardID, err) + } + } + + return nil +} + +func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block, asTemplate bool) (map[string]string, error) { + // Images attached in cards have a path comprising the card's board ID. + // When we create a template from this board, we need to copy the files + // with the new board ID in path. + // Not doing so causing images in templates (and boards created from this + // template) to fail to load. + + // look up ID of source sourceBoard, which may be different than the blocks. + sourceBoard, err := a.GetBoard(sourceBoardID) + if err != nil || sourceBoard == nil { + return nil, fmt.Errorf("cannot fetch source board %s for CopyCardFiles: %w", sourceBoardID, err) + } + + var destBoard *model.Board + newFileNames := make(map[string]string) + for _, block := range copiedBlocks { + if block.Type != model.TypeImage && block.Type != model.TypeAttachment { + continue + } + + fileId, isOk := block.Fields["fileId"].(string) + if !isOk { + fileId, isOk = block.Fields["attachmentId"].(string) + if !isOk { + continue + } + } + + // create unique filename + ext := filepath.Ext(fileId) + fileInfoID := utils.NewID(utils.IDTypeNone) + destFilename := fileInfoID + ext + + if destBoard == nil || block.BoardID != destBoard.ID { + destBoard = sourceBoard + if block.BoardID != destBoard.ID { + destBoard, err = a.GetBoard(block.BoardID) + if err != nil { + return nil, fmt.Errorf("cannot fetch destination board %s for CopyCardFiles: %w", sourceBoardID, err) + } + } + } + + // GetFilePath will retrieve the correct path + // depending on whether FileInfo table is used for the file. + fileInfo, sourceFilePath, err := a.GetFilePath(sourceBoard.TeamID, sourceBoard.ID, fileId) + if err != nil { + return nil, fmt.Errorf("cannot fetch destination board %s for CopyCardFiles: %w", sourceBoardID, err) + } + destinationFilePath := getDestinationFilePath(asTemplate, destBoard.TeamID, destBoard.ID, destFilename) + + if fileInfo == nil { + fileInfo = model.NewFileInfo(destFilename) + } + fileInfo.Id = getFileInfoID(fileInfoID) + fileInfo.Path = destinationFilePath + err = a.store.SaveFileInfo(fileInfo) + if err != nil { + return nil, fmt.Errorf("CopyCardFiles: cannot create fileinfo: %w", err) + } + + a.logger.Debug( + "Copying card file", + mlog.String("sourceFilePath", sourceFilePath), + mlog.String("destinationFilePath", destinationFilePath), + ) + + if err := a.filesBackend.CopyFile(sourceFilePath, destinationFilePath); err != nil { + a.logger.Error( + "CopyCardFiles failed to copy file", + mlog.String("sourceFilePath", sourceFilePath), + mlog.String("destinationFilePath", destinationFilePath), + mlog.Err(err), + ) + } + newFileNames[fileId] = destFilename + } + + return newFileNames, nil +} diff --git a/server/boards/app/files_test.go b/server/boards/app/files_test.go index 229712896b..c494b8a872 100644 --- a/server/boards/app/files_test.go +++ b/server/boards/app/files_test.go @@ -15,6 +15,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/mattermost/mattermost-server/server/v8/boards/model" mm_model "github.com/mattermost/mattermost-server/server/v8/model" "github.com/mattermost/mattermost-server/server/v8/platform/shared/filestore" "github.com/mattermost/mattermost-server/server/v8/platform/shared/filestore/mocks" @@ -210,7 +211,7 @@ func TestSaveFile(t *testing.T) { } mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) - actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testBoardID, fileName) + actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testBoardID, fileName, false) assert.Equal(t, fileName, actual) assert.NoError(t, err) }) @@ -234,7 +235,7 @@ func TestSaveFile(t *testing.T) { } mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) - actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName) + actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName, false) assert.NoError(t, err) assert.NotNil(t, actual) }) @@ -258,7 +259,7 @@ func TestSaveFile(t *testing.T) { } mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) - actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName) + actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName, false) assert.Equal(t, "", actual) assert.Equal(t, "unable to store the file in the files storage: Mocked File backend error", err.Error()) }) @@ -312,7 +313,7 @@ func TestGetFileInfo(t *testing.T) { func TestGetFile(t *testing.T) { th, _ := SetupTestHelper(t) - t.Run("when FileInfo exists", func(t *testing.T) { + t.Run("happy path, no errors", func(t *testing.T) { th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{ Id: "fileInfoID", Path: "/path/to/file/fileName.txt", @@ -337,27 +338,72 @@ func TestGetFile(t *testing.T) { assert.NotNil(t, seeker) }) - t.Run("when FileInfo doesn't exist", func(t *testing.T) { - th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, nil) + t.Run("when GetFilePath() throws error", func(t *testing.T) { + th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, errDummy) + + fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") + assert.Error(t, err) + assert.Nil(t, fileInfo) + assert.Nil(t, seeker) + }) + + t.Run("when FileExists returns false", func(t *testing.T) { + th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{ + Id: "fileInfoID", + Path: "/path/to/file/fileName.txt", + }, nil) mockedFileBackend := &mocks.FileBackend{} th.App.filesBackend = mockedFileBackend - mockedReadCloseSeek := &mocks.ReadCloseSeeker{} - readerFunc := func(path string) filestore.ReadCloseSeeker { - return mockedReadCloseSeek - } - - readerErrorFunc := func(path string) error { - return nil - } - - mockedFileBackend.On("Reader", "teamID/boardID/7fileInfoID.txt").Return(readerFunc, readerErrorFunc) - mockedFileBackend.On("FileExists", "teamID/boardID/7fileInfoID.txt").Return(true, nil) + mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").Return(false, nil) fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") + assert.Error(t, err) + assert.Nil(t, fileInfo) + assert.Nil(t, seeker) + }) + t.Run("when FileReader throws error", func(t *testing.T) { + th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{ + Id: "fileInfoID", + Path: "/path/to/file/fileName.txt", + }, nil) + + mockedFileBackend := &mocks.FileBackend{} + th.App.filesBackend = mockedFileBackend + mockedFileBackend.On("Reader", "/path/to/file/fileName.txt").Return(nil, errDummy) + mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").Return(true, nil) + + fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") + assert.Error(t, err) + assert.Nil(t, fileInfo) + assert.Nil(t, seeker) + }) + +} + +func TestGetFilePath(t *testing.T) { + th, _ := SetupTestHelper(t) + + t.Run("when FileInfo exists", func(t *testing.T) { + path := "/path/to/file/fileName.txt" + th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{ + Id: "fileInfoID", + Path: path, + }, nil) + + fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt") + assert.NoError(t, err) + assert.NotNil(t, fileInfo) + assert.Equal(t, path, filePath) + }) + + t.Run("when FileInfo doesn't exist", func(t *testing.T) { + th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, nil) + + fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt") assert.NoError(t, err) assert.Nil(t, fileInfo) - assert.NotNil(t, seeker) + assert.Equal(t, "teamID/boardID/7fileInfoID.txt", filePath) }) t.Run("when FileInfo exists but FileInfo.Path is not set", func(t *testing.T) { @@ -366,22 +412,158 @@ func TestGetFile(t *testing.T) { Path: "", }, nil) - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedReadCloseSeek := &mocks.ReadCloseSeeker{} - readerFunc := func(path string) filestore.ReadCloseSeeker { - return mockedReadCloseSeek - } - - readerErrorFunc := func(path string) error { - return nil - } - mockedFileBackend.On("Reader", "teamID/boardID/7fileInfoID.txt").Return(readerFunc, readerErrorFunc) - mockedFileBackend.On("FileExists", "teamID/boardID/7fileInfoID.txt").Return(true, nil) - - fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") + fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt") assert.NoError(t, err) assert.NotNil(t, fileInfo) - assert.NotNil(t, seeker) + assert.Equal(t, "teamID/boardID/7fileInfoID.txt", filePath) + }) +} + +func TestCopyCard(t *testing.T) { + th, _ := SetupTestHelper(t) + imageBlock := &model.Block{ + ID: "imageBlock", + ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske", + CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh", + ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh", + Schema: 1, + Type: "image", + Title: "", + Fields: map[string]interface{}{"fileId": "7fileName.jpg"}, + CreateAt: 1680725585250, + UpdateAt: 1680725585250, + DeleteAt: 0, + BoardID: "boardID", + } + t.Run("Board doesn't exist", func(t *testing.T) { + th.Store.EXPECT().GetBoard("boardID").Return(nil, errDummy) + _, err := th.App.CopyCardFiles("boardID", []*model.Block{}, false) + assert.Error(t, err) + }) + + t.Run("Board exists, image block, with FileInfo", func(t *testing.T) { + path := "/path/to/file/fileName.txt" + fileInfo := &mm_model.FileInfo{ + Id: "imageBlock", + Path: path, + } + th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{ + ID: "boardID", + IsTemplate: false, + }, nil) + th.Store.EXPECT().GetFileInfo("fileName").Return(fileInfo, nil) + th.Store.EXPECT().SaveFileInfo(fileInfo).Return(nil) + + mockedFileBackend := &mocks.FileBackend{} + th.App.filesBackend = mockedFileBackend + mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil) + + updatedFileNames, err := th.App.CopyCardFiles("boardID", []*model.Block{imageBlock}, false) + assert.NoError(t, err) + assert.Equal(t, "7fileName.jpg", imageBlock.Fields["fileId"]) + assert.NotNil(t, updatedFileNames["7fileName.jpg"]) + assert.NotNil(t, updatedFileNames[imageBlock.Fields["fileId"].(string)]) + }) + + t.Run("Board exists, attachment block, with FileInfo", func(t *testing.T) { + attachmentBlock := &model.Block{ + ID: "attachmentBlock", + ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske", + CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh", + ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh", + Schema: 1, + Type: "attachment", + Title: "", + Fields: map[string]interface{}{"fileId": "7fileName.jpg"}, + CreateAt: 1680725585250, + UpdateAt: 1680725585250, + DeleteAt: 0, + BoardID: "boardID", + } + + path := "/path/to/file/fileName.txt" + fileInfo := &mm_model.FileInfo{ + Id: "attachmentBlock", + Path: path, + } + th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{ + ID: "boardID", + IsTemplate: false, + }, nil) + th.Store.EXPECT().GetFileInfo("fileName").Return(fileInfo, nil) + th.Store.EXPECT().SaveFileInfo(fileInfo).Return(nil) + + mockedFileBackend := &mocks.FileBackend{} + th.App.filesBackend = mockedFileBackend + mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil) + + updatedFileNames, err := th.App.CopyCardFiles("boardID", []*model.Block{attachmentBlock}, false) + assert.NoError(t, err) + assert.NotNil(t, updatedFileNames[imageBlock.Fields["fileId"].(string)]) + }) + + t.Run("Board exists, image block, without FileInfo", func(t *testing.T) { + // path := "/path/to/file/fileName.txt" + // fileInfo := &mm_model.FileInfo{ + // Id: "imageBlock", + // Path: path, + // } + th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{ + ID: "boardID", + IsTemplate: false, + }, nil) + th.Store.EXPECT().GetFileInfo(gomock.Any()).Return(nil, nil) + th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) + + mockedFileBackend := &mocks.FileBackend{} + th.App.filesBackend = mockedFileBackend + mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil) + + updatedFileNames, err := th.App.CopyCardFiles("boardID", []*model.Block{imageBlock}, false) + assert.NoError(t, err) + assert.NotNil(t, imageBlock.Fields["fileId"].(string)) + assert.NotNil(t, updatedFileNames[imageBlock.Fields["fileId"].(string)]) + }) +} + +func TestCopyAndUpdateCardFiles(t *testing.T) { + th, _ := SetupTestHelper(t) + imageBlock := &model.Block{ + ID: "imageBlock", + ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske", + CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh", + ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh", + Schema: 1, + Type: "image", + Title: "", + Fields: map[string]interface{}{"fileId": "7fileName.jpg"}, + CreateAt: 1680725585250, + UpdateAt: 1680725585250, + DeleteAt: 0, + BoardID: "boardID", + } + + t.Run("Board exists, image block, with FileInfo", func(t *testing.T) { + path := "/path/to/file/fileName.txt" + fileInfo := &mm_model.FileInfo{ + Id: "imageBlock", + Path: path, + } + th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{ + ID: "boardID", + IsTemplate: false, + }, nil) + th.Store.EXPECT().GetFileInfo("fileName").Return(fileInfo, nil) + th.Store.EXPECT().SaveFileInfo(fileInfo).Return(nil) + th.Store.EXPECT().PatchBlocks(gomock.Any(), "userID").Return(nil) + + mockedFileBackend := &mocks.FileBackend{} + th.App.filesBackend = mockedFileBackend + mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil) + + err := th.App.CopyAndUpdateCardFiles("boardID", "userID", []*model.Block{imageBlock}, false) + assert.NoError(t, err) + + assert.NotEqual(t, path, imageBlock.Fields["fileId"]) }) } diff --git a/server/boards/app/import.go b/server/boards/app/import.go index 7f694e2717..1b30c09a88 100644 --- a/server/boards/app/import.go +++ b/server/boards/app/import.go @@ -44,27 +44,19 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error { a.logger.Debug("importing legacy archive") _, errImport := a.ImportBoardJSONL(br, opt) - go func() { - if err := a.UpdateCardLimitTimestamp(); err != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after importing a legacy file", - mlog.Err(err), - ) - } - }() - return errImport } - a.logger.Debug("importing archive") zr := zipstream.NewReader(br) - boardMap := make(map[string]string) // maps old board ids to new + boardMap := make(map[string]*model.Board) // maps old board ids to new + fileMap := make(map[string]string) // maps old fileIds to new for { hdr, err := zr.Next() if err != nil { if errors.Is(err, io.EOF) { + a.fixImagesAttachments(boardMap, fileMap, opt.TeamID, opt.ModifiedBy) a.logger.Debug("import archive - done", mlog.Int("boards_imported", len(boardMap))) return nil } @@ -84,14 +76,14 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error { return model.NewErrUnsupportedArchiveVersion(ver, archiveVersion) } case "board.jsonl": - boardID, err := a.ImportBoardJSONL(zr, opt) + board, err := a.ImportBoardJSONL(zr, opt) if err != nil { return fmt.Errorf("cannot import board %s: %w", dir, err) } - boardMap[dir] = boardID + boardMap[dir] = board default: // import file/image; dir is the old board id - boardID, ok := boardMap[dir] + board, ok := boardMap[dir] if !ok { a.logger.Warn("skipping orphan image in archive", mlog.String("dir", dir), @@ -99,33 +91,63 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error { ) continue } - // save file with original filename so it matches name in image block. - filePath := filepath.Join(opt.TeamID, boardID, filename) - _, err := a.filesBackend.WriteFile(zr, filePath) + + newFileName, err := a.SaveFile(zr, opt.TeamID, board.ID, filename, board.IsTemplate) if err != nil { return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, err) } + fileMap[filename] = newFileName + + a.logger.Debug("import archive file", + mlog.String("TeamID", opt.TeamID), + mlog.String("boardID", board.ID), + mlog.String("filename", filename), + mlog.String("newFileName", newFileName), + ) + } + } +} + +// Update image and attachment blocks +func (a *App) fixImagesAttachments(boardMap map[string]*model.Board, fileMap map[string]string, teamID string, userId string) { + blockIDs := make([]string, 0) + blockPatches := make([]model.BlockPatch, 0) + for _, board := range boardMap { + if board.IsTemplate { + continue } - a.logger.Trace("import archive file", - mlog.String("dir", dir), - mlog.String("filename", filename), - ) + opts := model.QueryBlocksOptions{ + BoardID: board.ID, + } + newBlocks, err := a.GetBlocks(opts) + if err != nil { + a.logger.Info("cannot retrieve imported blocks for board", mlog.String("BoardID", board.ID), mlog.Err(err)) + return + } - go func() { - if err := a.UpdateCardLimitTimestamp(); err != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after importing an archive", - mlog.Err(err), - ) + for _, block := range newBlocks { + if block.Type == "image" || block.Type == "attachment" { + fieldName := "fileId" + oldId := block.Fields[fieldName] + blockIDs = append(blockIDs, block.ID) + + blockPatches = append(blockPatches, model.BlockPatch{ + UpdatedFields: map[string]interface{}{ + fieldName: fileMap[oldId.(string)], + }, + }) } - }() + } + + blockPatchBatch := model.BlockPatchBatch{BlockIDs: blockIDs, BlockPatches: blockPatches} + a.PatchBlocks(teamID, &blockPatchBatch, userId) } } // ImportBoardJSONL imports a JSONL file containing blocks for one board. The resulting // board id is returned. -func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (string, error) { +func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (*model.Board, error) { // TODO: Stream this once `model.GenerateBlockIDs` can take a stream of blocks. // We don't want to load the whole file in memory, even though it's a single board. boardsAndBlocks := &model.BoardsAndBlocks{ @@ -158,7 +180,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str if !skip { var archiveLine model.ArchiveLine if err := json.Unmarshal(line, &archiveLine); err != nil { - return "", fmt.Errorf("error parsing archive line %d: %w", lineNum, err) + return nil, fmt.Errorf("error parsing archive line %d: %w", lineNum, err) } // first line must be a board @@ -170,7 +192,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str case "board": var board model.Board if err2 := json.Unmarshal(archiveLine.Data, &board); err2 != nil { - return "", fmt.Errorf("invalid board in archive line %d: %w", lineNum, err2) + return nil, fmt.Errorf("invalid board in archive line %d: %w", lineNum, err2) } board.ModifiedBy = userID board.UpdateAt = now @@ -181,20 +203,20 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str // legacy archives encoded boards as blocks; we need to convert them to real boards. var block *model.Block if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil { - return "", fmt.Errorf("invalid board block in archive line %d: %w", lineNum, err2) + return nil, fmt.Errorf("invalid board block in archive line %d: %w", lineNum, err2) } block.ModifiedBy = userID block.UpdateAt = now board, err := a.blockToBoard(block, opt) if err != nil { - return "", fmt.Errorf("cannot convert archive line %d to block: %w", lineNum, err) + return nil, fmt.Errorf("cannot convert archive line %d to block: %w", lineNum, err) } boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, board) boardID = board.ID case "block": var block *model.Block if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil { - return "", fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2) + return nil, fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2) } block.ModifiedBy = userID block.UpdateAt = now @@ -203,11 +225,11 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str case "boardMember": var boardMember *model.BoardMember if err2 := json.Unmarshal(archiveLine.Data, &boardMember); err2 != nil { - return "", fmt.Errorf("invalid board Member in archive line %d: %w", lineNum, err2) + return nil, fmt.Errorf("invalid board Member in archive line %d: %w", lineNum, err2) } boardMembers = append(boardMembers, boardMember) default: - return "", model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type) + return nil, model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type) } firstLine = false } @@ -217,7 +239,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str if errors.Is(errRead, io.EOF) { break } - return "", fmt.Errorf("error reading archive line %d: %w", lineNum, errRead) + return nil, fmt.Errorf("error reading archive line %d: %w", lineNum, errRead) } lineNum++ } @@ -234,12 +256,12 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str var err error boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger) if err != nil { - return "", fmt.Errorf("error generating archive block IDs: %w", err) + return nil, fmt.Errorf("error generating archive block IDs: %w", err) } boardsAndBlocks, err = a.CreateBoardsAndBlocks(boardsAndBlocks, opt.ModifiedBy, false) if err != nil { - return "", fmt.Errorf("error inserting archive blocks: %w", err) + return nil, fmt.Errorf("error inserting archive blocks: %w", err) } // add users to all the new boards (if not the fake system user). @@ -251,7 +273,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str SchemeAdmin: true, } if _, err2 := a.AddMemberToBoard(adminMember); err2 != nil { - return "", fmt.Errorf("cannot add adminMember to board: %w", err2) + return nil, fmt.Errorf("cannot add adminMember to board: %w", err2) } for _, boardMember := range boardMembers { bm := &model.BoardMember{ @@ -266,16 +288,16 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str Synthetic: boardMember.Synthetic, } if _, err2 := a.AddMemberToBoard(bm); err2 != nil { - return "", fmt.Errorf("cannot add member to board: %w", err2) + return nil, fmt.Errorf("cannot add member to board: %w", err2) } } } // find new board id for _, board := range boardsAndBlocks.Boards { - return board.ID, nil + return board, nil } - return "", fmt.Errorf("missing board in archive: %w", model.ErrInvalidBoardBlock) + return nil, fmt.Errorf("missing board in archive: %w", model.ErrInvalidBoardBlock) } // fixBoardsandBlocks allows the caller of `ImportArchive` to modify or filters boards and blocks being diff --git a/server/boards/app/import_test.go b/server/boards/app/import_test.go index 89d73335bf..061549cbad 100644 --- a/server/boards/app/import_test.go +++ b/server/boards/app/import_test.go @@ -138,9 +138,76 @@ func TestApp_ImportArchive(t *testing.T) { th.Store.EXPECT().GetUserByID("hxxzooc3ff8cubsgtcmpn8733e").AnyTimes().Return(user2, nil) th.Store.EXPECT().GetUserByID("nto73edn5ir6ifimo5a53y1dwa").AnyTimes().Return(user3, nil) - boardID, err := th.App.ImportBoardJSONL(r, opts) - require.Equal(t, board.ID, boardID, "Board ID should be same") + newBoard, err := th.App.ImportBoardJSONL(r, opts) require.NoError(t, err, "import archive should not fail") + require.Equal(t, board.ID, newBoard.ID, "Board ID should be same") + }) + + t.Run("fix image and attachment", func(t *testing.T) { + boardMap := map[string]*model.Board{ + "test": board, + } + + fileMap := map[string]string{ + "oldFileName1.jpg": "newFileName1.jpg", + "oldFileName2.jpg": "newFileName2.jpg", + } + + imageBlock := &model.Block{ + ID: "blockID-1", + ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske", + CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh", + ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh", + Schema: 1, + Type: "image", + Title: "", + Fields: map[string]interface{}{"fileId": "oldFileName1.jpg"}, + CreateAt: 1680725585250, + UpdateAt: 1680725585250, + DeleteAt: 0, + BoardID: "board-id", + } + + attachmentBlock := &model.Block{ + ID: "blockID-2", + ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske", + CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh", + ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh", + Schema: 1, + Type: "attachment", + Title: "", + Fields: map[string]interface{}{"fileId": "oldFileName2.jpg"}, + CreateAt: 1680725585250, + UpdateAt: 1680725585250, + DeleteAt: 0, + BoardID: "board-id", + } + + blockIDs := []string{"blockID-1", "blockID-2"} + + blockPatch := model.BlockPatch{ + UpdatedFields: map[string]interface{}{"fileId": "newFileName1.jpg"}, + } + + blockPatch2 := model.BlockPatch{ + UpdatedFields: map[string]interface{}{"fileId": "newFileName2.jpg"}, + } + + blockPatches := []model.BlockPatch{blockPatch, blockPatch2} + + blockPatchesBatch := model.BlockPatchBatch{BlockIDs: blockIDs, BlockPatches: blockPatches} + + opts := model.QueryBlocksOptions{ + BoardID: board.ID, + } + th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{imageBlock, attachmentBlock}, nil) + th.Store.EXPECT().GetBlocksByIDs(blockIDs).Return([]*model.Block{imageBlock, attachmentBlock}, nil) + th.Store.EXPECT().GetBlock(blockIDs[0]).Return(imageBlock, nil) + th.Store.EXPECT().GetBlock(blockIDs[1]).Return(attachmentBlock, nil) + th.Store.EXPECT().GetMembersForBoard("board-id").AnyTimes().Return([]*model.BoardMember{}, nil) + + th.Store.EXPECT().PatchBlocks(&blockPatchesBatch, "my-userid") + th.App.fixImagesAttachments(boardMap, fileMap, "test-team", "my-userid") }) } diff --git a/server/boards/app/templates_test.go b/server/boards/app/templates_test.go index bb1eb9bd3a..496929a7ef 100644 --- a/server/boards/app/templates_test.go +++ b/server/boards/app/templates_test.go @@ -53,6 +53,7 @@ func TestApp_initializeTemplates(t *testing.T) { th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{}, nil) th.Store.EXPECT().GetBoard(board.ID).AnyTimes().Return(board, nil) th.Store.EXPECT().GetMemberForBoard(gomock.Any(), gomock.Any()).AnyTimes().Return(boardMember, nil) + th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil).AnyTimes() th.FilesBackend.On("WriteFile", mock.Anything, mock.Anything).Return(int64(1), nil) diff --git a/server/boards/integrationtests/permissions_test.go b/server/boards/integrationtests/permissions_test.go index 936e15bfea..a784ef2bf3 100644 --- a/server/boards/integrationtests/permissions_test.go +++ b/server/boards/integrationtests/permissions_test.go @@ -3379,7 +3379,7 @@ func TestPermissionsGetFile(t *testing.T) { clients := setupClients(th) testData := setupData(t, th) - newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png") + newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png", false) require.NoError(t, err) ttCases := ttCasesF() @@ -3394,7 +3394,7 @@ func TestPermissionsGetFile(t *testing.T) { clients := setupLocalClients(th) testData := setupData(t, th) - newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png") + newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png", false) require.NoError(t, err) ttCases := ttCasesF() diff --git a/server/boards/model/file.go b/server/boards/model/file.go new file mode 100644 index 0000000000..3a00008cac --- /dev/null +++ b/server/boards/model/file.go @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +package model + +import ( + "mime" + "path/filepath" + "strings" + + "github.com/mattermost/mattermost-server/server/v8/boards/utils" + mm_model "github.com/mattermost/mattermost-server/server/v8/model" +) + +func NewFileInfo(name string) *mm_model.FileInfo { + + extension := strings.ToLower(filepath.Ext(name)) + now := utils.GetMillis() + return &mm_model.FileInfo{ + CreatorId: "boards", + CreateAt: now, + UpdateAt: now, + Name: name, + Extension: extension, + MimeType: mime.TypeByExtension(extension), + } + +} diff --git a/server/boards/services/store/sqlstore/schema_table_migration.go b/server/boards/services/store/sqlstore/schema_table_migration.go index cdb0f4d628..8ec5f154a9 100644 --- a/server/boards/services/store/sqlstore/schema_table_migration.go +++ b/server/boards/services/store/sqlstore/schema_table_migration.go @@ -126,7 +126,7 @@ func (s *SQLStore) isSchemaMigrationNeeded() (bool, error) { case model.MysqlDBType: query = query.Where(sq.Eq{"TABLE_SCHEMA": s.schemaName}) case model.PostgresDBType: - query = query.Where(sq.Eq{"TABLE_SCHEMA": "current_schema()"}) + query = query.Where("table_schema = current_schema()") } rows, err := query.Query() diff --git a/server/build/Dockerfile b/server/build/Dockerfile index 294debc3f9..8017e620f1 100644 --- a/server/build/Dockerfile +++ b/server/build/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:buster-slim@sha256:5b0b1a9a54651bbe9d4d3ee96bbda2b2a1da3d2fa198ddebbced46dfdca7f216 +FROM ubuntu:jammy-20230308@sha256:7a57c69fe1e9d5b97c5fe649849e79f2cfc3bf11d10bbd5218b4eb61716aebe6 # Setting bash as our shell, and enabling pipefail option @@ -13,68 +13,13 @@ ARG MM_PACKAGE="https://releases.mattermost.com/7.10.0/mattermost-7.10.0-linux-a # # Install needed packages and indirect dependencies RUN apt-get update \ && apt-get install --no-install-recommends -y \ - ca-certificates=20200601~deb10u2 \ - curl=7.64.0-4+deb10u2 \ - mime-support=3.62 \ - unrtf=0.21.10-clean-1 \ - wv=1.2.9-4.2+b2 \ - poppler-utils=0.71.0-5 \ - tidy=2:5.6.0-10 \ - libssl1.1=1.1.1n-0+deb10u3 \ - sensible-utils=0.0.12 \ - libsasl2-modules-db=2.1.27+dfsg-1+deb10u2 \ - libsasl2-2=2.1.27+dfsg-1+deb10u2 \ - libldap-common=2.4.47+dfsg-3+deb10u7 \ - libldap-2.4-2=2.4.47+dfsg-3+deb10u7 \ - libicu63=63.1-6+deb10u3 \ - libxml2=2.9.4+dfsg1-7+deb10u4 \ - ucf=3.0038+nmu1 \ - openssl=1.1.1n-0+deb10u3 \ - libkeyutils1=1.6-6 \ - libkrb5support0=1.17-3+deb10u4 \ - libk5crypto3=1.17-3+deb10u4 \ - libkrb5-3=1.17-3+deb10u4 \ - libgssapi-krb5-2=1.17-3+deb10u4 \ - libnghttp2-14=1.36.0-2+deb10u1 \ - libpsl5=0.20.2-2 \ - librtmp1=2.4+20151223.gitfa8646d.1-2 \ - libssh2-1=1.8.0-2.1 \ - libcurl4=7.64.0-4+deb10u2 \ - fonts-dejavu-core=2.37-1 \ - fontconfig-config=2.13.1-2 \ - libbsd0=0.9.1-2+deb10u1 \ - libexpat1=2.2.6-2+deb10u4 \ - libpng16-16=1.6.36-6 \ - libfreetype6=2.9.1-3+deb10u2 \ - libfontconfig1=2.13.1-2 \ - libpixman-1-0=0.36.0-1 \ - libxau6=1:1.0.8-1+b2 \ - libxdmcp6=1:1.1.2-3 \ - libxcb1=1.13.1-2 \ - libx11-data=2:1.6.7-1+deb10u2 \ - libx11-6=2:1.6.7-1+deb10u2 \ - libxcb-render0=1.13.1-2 \ - libxcb-shm0=1.13.1-2 \ - libxext6=2:1.3.3-1+b2 \ - libxrender1=1:0.9.10-1 \ - libcairo2=1.16.0-4+deb10u1 \ - libcurl3-gnutls=7.64.0-4+deb10u5 \ - libglib2.0-0=2.58.3-2+deb10u3 \ - libgsf-1-common=1.14.45-1 \ - libgsf-1-114=1.14.45-1 \ - libjbig0=2.1-3.1+b2 \ - libjpeg62-turbo=1:1.5.2-2+deb10u1 \ - liblcms2-2=2.9-3 \ - libnspr4=2:4.20-1 \ - libsqlite3-0=3.27.2-3+deb10u1 \ - libnss3=2:3.42.1-1+deb10u5 \ - libopenjp2-7=2.3.0-2+deb10u2 \ - libwebp6=0.6.1-2+deb10u1 \ - libtiff5=4.1.0+git191117-2~deb10u4 \ - libpoppler82=0.71.0-5 \ - libtidy5deb1=2:5.6.0-10 \ - libwmf0.2-7=0.2.8.4-14 \ - libwv-1.2-4=1.2.9-4.2+b2 \ + ca-certificates \ + curl \ + mime-support \ + unrtf \ + wv \ + poppler-utils \ + tidy \ && rm -rf /var/lib/apt/lists/* # Set mattermost group/user and download Mattermost diff --git a/server/channels/api4/command.go b/server/channels/api4/command.go index 312c0d093f..724994fe01 100644 --- a/server/channels/api4/command.go +++ b/server/channels/api4/command.go @@ -329,13 +329,6 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { 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 - } - channel, err := c.App.GetChannel(c.AppContext, commandArgs.ChannelId) if err != nil { c.Err = err @@ -354,13 +347,6 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { 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 - } } } diff --git a/server/channels/api4/plugin_test.go b/server/channels/api4/plugin_test.go index 98ff5ac3f1..8a923d1d0c 100644 --- a/server/channels/api4/plugin_test.go +++ b/server/channels/api4/plugin_test.go @@ -1722,14 +1722,26 @@ func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) { appErr := th.App.AddPublicKey("pub_key", key) require.Nil(t, appErr) + t.Cleanup(func() { + appErr = th.App.DeletePublicKey("pub_key") + require.Nil(t, appErr) + }) + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { serverVersion := req.URL.Query().Get("server_version") require.NotEmpty(t, serverVersion) require.Equal(t, model.CurrentVersion, serverVersion) res.WriteHeader(http.StatusOK) + var out []byte - out, err = json.Marshal([]*model.MarketplacePlugin{samplePlugins[1]}) - require.NoError(t, err) + + // Return something if testplugin2 or no specific plugin is requested + pluginID := req.URL.Query().Get("plugin_id") + if pluginID == "" || pluginID == samplePlugins[1].Manifest.Id { + out, err = json.Marshal([]*model.MarketplacePlugin{samplePlugins[1]}) + require.NoError(t, err) + } + res.Write(out) })) defer testServer.Close() @@ -1748,43 +1760,52 @@ func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) { require.Len(t, pluginsResp.Active, 0) require.Len(t, pluginsResp.Inactive, 0) - // Should fail to install unknown prepackaged plugin - pRequest := &model.InstallMarketplacePluginRequest{Id: "testpluginXX"} - manifest, resp, err := client.InstallMarketplacePlugin(pRequest) - require.Error(t, err) - CheckInternalErrorStatus(t, resp) - require.Nil(t, manifest) + t.Run("Should fail to install unknown prepackaged plugin", func(t *testing.T) { + pRequest := &model.InstallMarketplacePluginRequest{Id: "testpluginXX"} + manifest, resp, err := client.InstallMarketplacePlugin(pRequest) + require.Error(t, err) + CheckInternalErrorStatus(t, resp) + require.Nil(t, manifest) - plugins := env.PrepackagedPlugins() - require.Len(t, plugins, 1) - require.Equal(t, "testplugin", plugins[0].Manifest.Id) - require.Equal(t, pluginSignatureData, plugins[0].Signature) + plugins := env.PrepackagedPlugins() + require.Len(t, plugins, 1) + require.Equal(t, "testplugin", plugins[0].Manifest.Id) + require.Equal(t, pluginSignatureData, plugins[0].Signature) - pluginsResp, _, err = client.GetPlugins() - require.NoError(t, err) - require.Len(t, pluginsResp.Active, 0) - require.Len(t, pluginsResp.Inactive, 0) + pluginsResp, _, err = client.GetPlugins() + require.NoError(t, err) + require.Len(t, pluginsResp.Active, 0) + require.Len(t, pluginsResp.Inactive, 0) + }) - pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin"} - manifest1, _, err := client.InstallMarketplacePlugin(pRequest) - require.NoError(t, err) - require.NotNil(t, manifest1) - require.Equal(t, "testplugin", manifest1.Id) - require.Equal(t, "0.0.1", manifest1.Version) + t.Run("Install prepackaged plugin with Marketplace disabled", func(t *testing.T) { + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"} + manifest, _, err := client.InstallMarketplacePlugin(pRequest) + require.NoError(t, err) + require.NotNil(t, manifest) + require.Equal(t, "testplugin", manifest.Id) + require.Equal(t, "0.0.1", manifest.Version) - pluginsResp, _, err = client.GetPlugins() - require.NoError(t, err) - require.Len(t, pluginsResp.Active, 0) - require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{ - Manifest: *manifest1, - }}) + t.Cleanup(func() { + _, err = client.RemovePlugin(manifest.Id) + require.NoError(t, err) + }) - // Try to install remote marketplace plugin - pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2"} - manifest, resp, err = client.InstallMarketplacePlugin(pRequest) - require.Error(t, err) - CheckInternalErrorStatus(t, resp) - require.Nil(t, manifest) + pluginsResp, _, err = client.GetPlugins() + require.NoError(t, err) + require.Len(t, pluginsResp.Active, 0) + require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{ + Manifest: *manifest, + }}) + }) + + t.Run("Try to install remote marketplace plugin while Marketplace is disabled", func(t *testing.T) { + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin2"} + manifest, resp, err := client.InstallMarketplacePlugin(pRequest) + require.Error(t, err) + CheckInternalErrorStatus(t, resp) + require.Nil(t, manifest) + }) // Enable remote marketplace th.App.UpdateConfig(func(cfg *model.Config) { @@ -1794,31 +1815,58 @@ func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) { *cfg.PluginSettings.AllowInsecureDownloadURL = true }) - pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2"} - manifest2, _, err := client.InstallMarketplacePlugin(pRequest) - require.NoError(t, err) - require.NotNil(t, manifest2) - require.Equal(t, "testplugin2", manifest2.Id) - require.Equal(t, "1.2.3", manifest2.Version) + t.Run("Install prepackaged, not listed plugin with Marketplace enabled", func(t *testing.T) { + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"} + manifest, _, err := client.InstallMarketplacePlugin(pRequest) + require.NoError(t, err) - pluginsResp, _, err = client.GetPlugins() - require.NoError(t, err) - require.Len(t, pluginsResp.Active, 0) - require.ElementsMatch(t, pluginsResp.Inactive, []*model.PluginInfo{ - { - Manifest: *manifest1, - }, - { - Manifest: *manifest2, - }, + t.Cleanup(func() { + _, err = client.RemovePlugin(manifest.Id) + require.NoError(t, err) + }) + + require.NotNil(t, manifest) + assert.Equal(t, "testplugin", manifest.Id) + assert.Equal(t, "0.0.1", manifest.Version) }) - // Clean up - _, err = client.RemovePlugin(manifest1.Id) - require.NoError(t, err) + t.Run("Install both a prepacked and a Marketplace plugin", func(t *testing.T) { + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"} + manifest1, _, err := client.InstallMarketplacePlugin(pRequest) + require.NoError(t, err) + require.NotNil(t, manifest1) + assert.Equal(t, "testplugin", manifest1.Id) + assert.Equal(t, "0.0.1", manifest1.Version) - _, err = client.RemovePlugin(manifest2.Id) - require.NoError(t, err) + t.Cleanup(func() { + _, err = client.RemovePlugin(manifest1.Id) + require.NoError(t, err) + }) + + pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2"} + manifest2, _, err := client.InstallMarketplacePlugin(pRequest) + require.NoError(t, err) + require.NotNil(t, manifest2) + require.Equal(t, "testplugin2", manifest2.Id) + require.Equal(t, "1.2.3", manifest2.Version) + + t.Cleanup(func() { + _, err = client.RemovePlugin(manifest2.Id) + require.NoError(t, err) + }) + + pluginsResp, _, err = client.GetPlugins() + require.NoError(t, err) + require.Len(t, pluginsResp.Active, 0) + require.ElementsMatch(t, pluginsResp.Inactive, []*model.PluginInfo{ + { + Manifest: *manifest1, + }, + { + Manifest: *manifest2, + }, + }) + }) appErr = th.App.DeletePublicKey("pub_key") require.Nil(t, appErr) diff --git a/server/channels/app/app_test.go b/server/channels/app/app_test.go index 22f221d47b..0ba8caff88 100644 --- a/server/channels/app/app_test.go +++ b/server/channels/app/app_test.go @@ -119,7 +119,6 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PermissionGetPublicLink.Id, model.PermissionCreatePost.Id, model.PermissionUseChannelMentions.Id, - model.PermissionUseSlashCommands.Id, model.PermissionManagePublicChannelProperties.Id, model.PermissionDeletePublicChannel.Id, model.PermissionManagePrivateChannelProperties.Id, diff --git a/server/channels/app/import_functions_test.go b/server/channels/app/import_functions_test.go index 95eb6ec64d..d540783603 100644 --- a/server/channels/app/import_functions_test.go +++ b/server/channels/app/import_functions_test.go @@ -459,7 +459,7 @@ func TestImportImportRole(t *testing.T) { // Try changing all the params and reimporting. data.DisplayName = ptrStr("new display name") data.Description = ptrStr("description") - data.Permissions = &[]string{"use_slash_commands"} + data.Permissions = &[]string{"manage_slash_commands"} err = th.App.importRole(th.Context, &data, false, true) require.Nil(t, err, "Should have succeeded. %v", err) diff --git a/server/channels/app/permissions_test.go b/server/channels/app/permissions_test.go index 9ae52a1605..37ced9bd92 100644 --- a/server/channels/app/permissions_test.go +++ b/server/channels/app/permissions_test.go @@ -114,7 +114,7 @@ func TestImportPermissions(t *testing.T) { } beforeCount = len(results) - json := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","use_slash_commands","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) + json := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) r := strings.NewReader(json) err := th.App.ImportPermissions(r) @@ -183,7 +183,7 @@ func TestImportPermissions_idempotentScheme(t *testing.T) { roleName1 := model.NewId() roleName2 := model.NewId() - json := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","use_slash_commands","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) + json := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) jsonl := strings.Repeat(json+"\n", 4) r := strings.NewReader(jsonl) @@ -226,7 +226,7 @@ func TestImportPermissions_schemeDeletedOnRoleFailure(t *testing.T) { roleName1 := model.NewId() roleName2 := model.NewId() - jsonl := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","use_slash_commands","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) + jsonl := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) r := strings.NewReader(jsonl) var results []*model.Scheme diff --git a/server/channels/app/plugin_install.go b/server/channels/app/plugin_install.go index de40c6838a..59ae9be613 100644 --- a/server/channels/app/plugin_install.go +++ b/server/channels/app/plugin_install.go @@ -203,35 +203,38 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl if *ch.cfgSvc.Config().PluginSettings.EnableRemoteMarketplace { var plugin *model.BaseMarketplacePlugin plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version) - if appErr != nil { - return nil, appErr + // The plugin might only be prepackaged and not on the Marketplace. + if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" { + mlog.Warn("Failed to reach Marketplace to install plugin", mlog.String("plugin_id", request.Id), mlog.Err(appErr)) } - var prepackagedVersion semver.Version - if prepackagedPlugin != nil { - var err error - prepackagedVersion, err = semver.Parse(prepackagedPlugin.Manifest.Version) - if err != nil { - return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err) + if plugin != nil { + var prepackagedVersion semver.Version + if prepackagedPlugin != nil { + var err error + prepackagedVersion, err = semver.Parse(prepackagedPlugin.Manifest.Version) + if err != nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err) + } } - } - marketplaceVersion, err := semver.Parse(plugin.Manifest.Version) - if err != nil { - return nil, model.NewAppError("InstallMarketplacePlugin", "app.prepackged-plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err) - } + marketplaceVersion, err := semver.Parse(plugin.Manifest.Version) + if err != nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.prepackged-plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err) + } - if prepackagedVersion.LT(marketplaceVersion) { // Always true if no prepackaged plugin was found - downloadedPluginBytes, err := ch.srv.downloadFromURL(plugin.DownloadURL) - if err != nil { - return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + if prepackagedVersion.LT(marketplaceVersion) { // Always true if no prepackaged plugin was found + downloadedPluginBytes, err := ch.srv.downloadFromURL(plugin.DownloadURL) + if err != nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + signature, err := plugin.DecodeSignature() + if err != nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, "", http.StatusNotImplemented).Wrap(err) + } + pluginFile = bytes.NewReader(downloadedPluginBytes) + signatureFile = signature } - signature, err := plugin.DecodeSignature() - if err != nil { - return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, "", http.StatusNotImplemented).Wrap(err) - } - pluginFile = bytes.NewReader(downloadedPluginBytes) - signatureFile = signature } } diff --git a/server/channels/app/worktemplates/generator/worktemplate.tmpl b/server/channels/app/worktemplates/generator/worktemplate.tmpl index 60f40b5301..52d8d0be5f 100644 --- a/server/channels/app/worktemplates/generator/worktemplate.tmpl +++ b/server/channels/app/worktemplates/generator/worktemplate.tmpl @@ -93,6 +93,7 @@ var wt{{.MD5}} = &WorkTemplate{ Illustration: "{{.Playbook.Illustration}}", },{{end}}{{if .Integration}}Integration: &Integration{ ID: "{{.Integration.ID}}", + Recommended: {{.Integration.Recommended}}, },{{end}} }, {{end}} diff --git a/server/channels/app/worktemplates/templates.yaml b/server/channels/app/worktemplates/templates.yaml index 351e034d15..362b014a71 100644 --- a/server/channels/app/worktemplates/templates.yaml +++ b/server/channels/app/worktemplates/templates.yaml @@ -45,8 +45,10 @@ content: illustration: "/static/worktemplates/playbooks/product_release.png" - integration: id: jira + recommended: true - integration: id: github + recommended: true --- id: 'product_teams/goals_and_okrs:v1' category: product_teams @@ -86,7 +88,7 @@ content: channel: channel-1674845108569 - integration: id: zoom - + recommended: true --- id: 'product_teams/bug_bash:v1' category: product_teams @@ -120,6 +122,7 @@ content: playbook: playbook-1674844017943 - integration: id: jira + recommended: true --- id: 'product_teams/sprint_planning:v1' category: product_teams @@ -153,6 +156,7 @@ content: channel: channel-1674850783500 - integration: id: zoom + recommended: true --- id: 'product_teams/product_roadmap:v1' category: product_teams @@ -282,6 +286,7 @@ content: channel: channel-1674845108569 - integration: id: zoom + recommended: true --- id: 'companywide/create_project:v1' category: companywide @@ -316,10 +321,13 @@ content: channel: channel-1674851940114 - integration: id: jira + recommended: true - integration: id: github + recommended: true - integration: id: zoom + recommended: true --- ###################### # Leadership @@ -356,4 +364,4 @@ content: channel: channel-1674845108569 - integration: id: zoom - + recommended: true diff --git a/server/channels/app/worktemplates/types.go b/server/channels/app/worktemplates/types.go index a75db605aa..83649a4571 100644 --- a/server/channels/app/worktemplates/types.go +++ b/server/channels/app/worktemplates/types.go @@ -108,7 +108,8 @@ func (wt WorkTemplate) ToModelWorkTemplate(t i18n.TranslateFunc) *model.WorkTemp if content.Integration != nil { mwt.Content = append(mwt.Content, model.WorkTemplateContent{ Integration: &model.WorkTemplateIntegration{ - ID: content.Integration.ID, + ID: content.Integration.ID, + Recommended: content.Integration.Recommended, }, }) } @@ -320,7 +321,8 @@ func (p *Playbook) Validate() error { } type Integration struct { - ID string `yaml:"id"` + ID string `yaml:"id"` + Recommended bool `yaml:"recommended"` } func (i *Integration) Validate() error { diff --git a/server/channels/app/worktemplates/worktemplate_generated.go b/server/channels/app/worktemplates/worktemplate_generated.go index a201d7c5c3..f7e3a3e16f 100644 --- a/server/channels/app/worktemplates/worktemplate_generated.go +++ b/server/channels/app/worktemplates/worktemplate_generated.go @@ -148,12 +148,14 @@ var wt00a1b44a5831c0a3acb14787b3fdd352 = &WorkTemplate{ }, { Integration: &Integration{ - ID: "jira", + ID: "jira", + Recommended: true, }, }, { Integration: &Integration{ - ID: "github", + ID: "github", + Recommended: true, }, }, }, @@ -214,7 +216,8 @@ var wt5baa68055bf9ea423273662e01ccc575 = &WorkTemplate{ }, { Integration: &Integration{ - ID: "zoom", + ID: "zoom", + Recommended: true, }, }, }, @@ -265,7 +268,8 @@ var wtfeb56bc6a8f277c47b503bd1c92d830e = &WorkTemplate{ }, { Integration: &Integration{ - ID: "jira", + ID: "jira", + Recommended: true, }, }, }, @@ -317,7 +321,8 @@ var wt8d2ef53deac5517eb349dc5de6150196 = &WorkTemplate{ }, { Integration: &Integration{ - ID: "zoom", + ID: "zoom", + Recommended: true, }, }, }, @@ -518,7 +523,8 @@ var wtf7b846d35810f8272eeb9a1a562025b5 = &WorkTemplate{ }, { Integration: &Integration{ - ID: "zoom", + ID: "zoom", + Recommended: true, }, }, }, @@ -570,17 +576,20 @@ var wtb9ab412890c2410c7b49eec8f12e7edc = &WorkTemplate{ }, { Integration: &Integration{ - ID: "jira", + ID: "jira", + Recommended: true, }, }, { Integration: &Integration{ - ID: "github", + ID: "github", + Recommended: true, }, }, { Integration: &Integration{ - ID: "zoom", + ID: "zoom", + Recommended: true, }, }, }, @@ -632,7 +641,8 @@ var wt32ab773bfe021e3d4913931041552559 = &WorkTemplate{ }, { Integration: &Integration{ - ID: "zoom", + ID: "zoom", + Recommended: true, }, }, }, diff --git a/server/channels/store/sqlstore/channel_store.go b/server/channels/store/sqlstore/channel_store.go index 2f8e91b9dc..38c35239c0 100644 --- a/server/channels/store/sqlstore/channel_store.go +++ b/server/channels/store/sqlstore/channel_store.go @@ -3035,7 +3035,8 @@ func (s SqlChannelStore) Autocomplete(userID, term string, includeDeleted, isGue sq.Expr("t.id = tm.TeamId"), sq.Eq{"tm.UserId": userID}, }). - OrderBy("c.DisplayName") + OrderBy("c.DisplayName"). + Limit(model.ChannelSearchDefaultLimit) if !includeDeleted { query = query.Where(sq.And{ @@ -3073,7 +3074,7 @@ func (s SqlChannelStore) Autocomplete(userID, term string, includeDeleted, isGue channels := model.ChannelListWithTeamData{} err = s.GetReplicaX().Select(&channels, sql, args...) if err != nil { - return nil, errors.Wrapf(err, "could not find channel with term=%s", term) + return nil, errors.Wrapf(err, "could not find channel with term=%s", trimInput(term)) } return channels, nil } @@ -3186,7 +3187,7 @@ func (s SqlChannelStore) AutocompleteInTeamForSearch(teamID string, userID strin // query the database err = s.GetReplicaX().Select(&channels, sql, args...) if err != nil { - return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", term) + return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", trimInput(term)) } directChannels, err := s.autocompleteInTeamForSearchDirectMessages(userID, term) @@ -3242,7 +3243,7 @@ func (s SqlChannelStore) autocompleteInTeamForSearchDirectMessages(userID string // query the channel list from the database using SQLX channels := model.ChannelList{} if err := s.GetReplicaX().Select(&channels, sql, args...); err != nil { - return nil, errors.Wrapf(err, "failed to find Channels with term='%s' (%s %% %v)", term, sql, args) + return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", trimInput(term)) } return channels, nil @@ -3461,7 +3462,7 @@ func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearch } channels := model.ChannelListWithTeamData{} if err2 := s.GetReplicaX().Select(&channels, queryString, args...); err2 != nil { - return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", term) + return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", trimInput(term)) } var totalCount int64 @@ -3474,7 +3475,7 @@ func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearch return nil, 0, errors.Wrap(err, "channel_tosql") } if err2 := s.GetReplicaX().Get(&totalCount, queryString, args...); err2 != nil { - return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", term) + return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", trimInput(term)) } } else { totalCount = int64(len(channels)) @@ -3651,7 +3652,7 @@ func (s SqlChannelStore) performSearch(searchQuery sq.SelectBuilder, term string channels := model.ChannelList{} err = s.GetReplicaX().Select(&channels, sql, args...) if err != nil { - return channels, errors.Wrapf(err, "failed to find Channels with term='%s'", term) + return channels, errors.Wrapf(err, "failed to find Channels with term='%s'", trimInput(term)) } return channels, nil @@ -3744,7 +3745,7 @@ func (s SqlChannelStore) SearchGroupChannels(userId, term string) (model.Channel groupChannels := model.ChannelList{} if err := s.GetReplicaX().Select(&groupChannels, sql, params...); err != nil { - return nil, errors.Wrapf(err, "failed to find Channels with term='%s' and userId=%s", term, userId) + return nil, errors.Wrapf(err, "failed to find Channels with term='%s' and userId=%s", trimInput(term), userId) } return groupChannels, nil } diff --git a/server/channels/store/sqlstore/file_info_store.go b/server/channels/store/sqlstore/file_info_store.go index 0e804605c9..52c5164136 100644 --- a/server/channels/store/sqlstore/file_info_store.go +++ b/server/channels/store/sqlstore/file_info_store.go @@ -681,7 +681,7 @@ func (fs SqlFileInfoStore) Search(paramsList []*model.SearchParams, userId, team items := []fileInfoWithChannelID{} err = fs.GetSearchReplicaX().Select(&items, queryString, args...) if err != nil { - mlog.Warn("Query error searching files.", mlog.Err(err)) + mlog.Warn("Query error searching files.", mlog.String("error", trimInput(err.Error()))) // Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results. } else { for _, item := range items { diff --git a/server/channels/store/sqlstore/post_store.go b/server/channels/store/sqlstore/post_store.go index e60583fe75..ad95fce3a9 100644 --- a/server/channels/store/sqlstore/post_store.go +++ b/server/channels/store/sqlstore/post_store.go @@ -2075,7 +2075,7 @@ func (s *SqlPostStore) search(teamId string, userId string, params *model.Search var posts []*model.Post if err := s.GetSearchReplicaX().Select(&posts, searchQuery, searchQueryArgs...); err != nil { - mlog.Warn("Query error searching posts.", mlog.Err(err)) + mlog.Warn("Query error searching posts.", mlog.String("error", trimInput(err.Error()))) // Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results. } else { for _, p := range posts { diff --git a/server/channels/store/sqlstore/utils.go b/server/channels/store/sqlstore/utils.go index 69d21ab824..753d5d3933 100644 --- a/server/channels/store/sqlstore/utils.go +++ b/server/channels/store/sqlstore/utils.go @@ -233,3 +233,14 @@ func SanitizeDataSource(driverName, dataSource string) (string, error) { return "", errors.New("invalid drivername. Not postgres or mysql.") } } + +const maxTokenSize = 50 + +// trimInput limits the string to a max size to prevent clogging up disk space +// while logging +func trimInput(input string) string { + if len(input) > maxTokenSize { + input = input[:maxTokenSize] + "..." + } + return input +} diff --git a/server/channels/store/storetest/channel_store.go b/server/channels/store/storetest/channel_store.go index 69b9328ec8..baffe97abd 100644 --- a/server/channels/store/storetest/channel_store.go +++ b/server/channels/store/storetest/channel_store.go @@ -115,7 +115,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("GetGuestCount", func(t *testing.T) { testGetGuestCount(t, ss) }) t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) }) t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) }) - t.Run("Autocomplete", func(t *testing.T) { testAutocomplete(t, ss) }) + t.Run("Autocomplete", func(t *testing.T) { testAutocomplete(t, ss, s) }) t.Run("SearchArchivedInTeam", func(t *testing.T) { testChannelStoreSearchArchivedInTeam(t, ss, s) }) t.Run("SearchForUserInTeam", func(t *testing.T) { testChannelStoreSearchForUserInTeam(t, ss) }) t.Run("SearchAllChannels", func(t *testing.T) { testChannelStoreSearchAllChannels(t, ss) }) @@ -5986,7 +5986,7 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) { } } -func testAutocomplete(t *testing.T, ss store.Store) { +func testAutocomplete(t *testing.T, ss store.Store, s SqlStore) { t1 := &model.Team{ DisplayName: "t1", Name: NewTestId(), @@ -6165,9 +6165,9 @@ func testAutocomplete(t *testing.T, ss store.Store) { } for _, testCase := range testCases { - t.Run("Autocomplete/"+testCase.Description, func(t *testing.T) { - channels, err := ss.Channel().Autocomplete(testCase.UserID, testCase.Term, testCase.IncludeDeleted, testCase.IsGuest) - require.NoError(t, err) + t.Run(testCase.Description, func(t *testing.T) { + channels, err2 := ss.Channel().Autocomplete(testCase.UserID, testCase.Term, testCase.IncludeDeleted, testCase.IsGuest) + require.NoError(t, err2) var gotChannelIds []string var gotTeamNames []string for _, ch := range channels { @@ -6178,6 +6178,24 @@ func testAutocomplete(t *testing.T, ss store.Store) { require.ElementsMatch(t, testCase.ExpectedTeamNames, gotTeamNames, "team names are not as expected") }) } + + t.Run("Limit", func(t *testing.T) { + for i := 0; i < model.ChannelSearchDefaultLimit+10; i++ { + _, err = ss.Channel().Save(&model.Channel{ + TeamId: teamID, + DisplayName: "Channel " + strconv.Itoa(i), + Name: NewTestId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + } + channels, err := ss.Channel().Autocomplete(m1.UserId, "Chann", false, false) + require.NoError(t, err) + assert.Len(t, channels, model.ChannelSearchDefaultLimit) + }) + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMasterX().Exec("TRUNCATE Channels") } func testChannelStoreSearchForUserInTeam(t *testing.T, ss store.Store) { diff --git a/server/channels/testlib/testdata/mysql_migration_warmup.sql b/server/channels/testlib/testdata/mysql_migration_warmup.sql index 070dae56f6..eaafb2d368 100644 --- a/server/channels/testlib/testdata/mysql_migration_warmup.sql +++ b/server/channels/testlib/testdata/mysql_migration_warmup.sql @@ -81,14 +81,14 @@ INSERT INTO `Roles` VALUES ('hkcrew7wttb5fbuw3ime6g7nzc','system_read_only_admin INSERT INTO `Roles` VALUES ('iiwt9pt6wiyb9e1enixtxs5yme','run_admin','authentication.roles.run_admin.name','authentication.roles.run_admin.description',1662271985864,1662271986932,0,' run_manage_properties run_manage_members',1,1); INSERT INTO `Roles` VALUES ('jg1f1xfh3bb73pua938orwg9ie','system_guest','authentication.roles.global_guest.name','authentication.roles.global_guest.description',1605167829015,1662271986937,0,' create_direct_channel create_group_channel',1,1); INSERT INTO `Roles` VALUES ('k891n5tpd3n9peue79azejjocy','system_post_all_public','authentication.roles.system_post_all_public.name','authentication.roles.system_post_all_public.description',0,1662271986941,0,' use_channel_mentions create_post_public',0,1); -INSERT INTO `Roles` VALUES ('kb6r9i58x7dxdb3srfohd66sse','system_admin','authentication.roles.global_admin.name','authentication.roles.global_admin.description',0,1662271986948,0,' list_public_teams edit_brand manage_private_channel_properties sysconsole_read_user_management_teams playbook_public_create manage_others_bots invalidate_caches manage_shared_channels sysconsole_write_environment_logging manage_others_outgoing_webhooks sysconsole_read_reporting_team_statistics sysconsole_read_plugins list_team_channels use_group_mentions sysconsole_read_site_users_and_teams sysconsole_write_site_localization get_analytics sysconsole_read_experimental_bleve manage_team_roles sysconsole_read_site_localization use_slash_commands edit_post sysconsole_write_user_management_channels test_elasticsearch list_private_teams add_ldap_public_cert join_public_teams manage_slash_commands manage_others_incoming_webhooks manage_public_channel_members sysconsole_read_environment_elasticsearch sysconsole_write_site_customization delete_others_emojis run_manage_members create_emojis sysconsole_write_authentication_email sysconsole_write_compliance_compliance_export add_saml_private_cert create_bot sysconsole_write_environment_rate_limiting add_saml_public_cert edit_other_users sysconsole_write_integrations_integration_management read_user_access_token create_elasticsearch_post_indexing_job sysconsole_write_user_management_users assign_system_admin_role sysconsole_write_user_management_groups sysconsole_read_authentication_guest_access sysconsole_write_about_edition_and_license sysconsole_read_authentication_ldap sysconsole_read_experimental_feature_flags sysconsole_read_integrations_cors sysconsole_read_user_management_groups join_public_channels sysconsole_read_experimental_features test_ldap sysconsole_write_environment_elasticsearch sysconsole_write_reporting_server_logs sysconsole_read_environment_image_proxy sysconsole_read_site_announcement_banner sysconsole_read_reporting_site_statistics sysconsole_write_authentication_mfa sysconsole_read_authentication_openid purge_bleve_indexes playbook_public_manage_members delete_emojis sysconsole_write_environment_file_storage sysconsole_write_reporting_site_statistics playbook_private_manage_members import_team sysconsole_write_environment_web_server sysconsole_write_authentication_password read_public_channel_groups create_compliance_export_job sysconsole_read_authentication_password list_users_without_team sysconsole_read_authentication_mfa add_ldap_private_cert create_data_retention_job read_license_information sysconsole_write_authentication_signup sysconsole_read_environment_push_notification_server edit_others_posts download_compliance_export_result create_ldap_sync_job sysconsole_write_authentication_ldap sysconsole_write_plugins read_data_retention_job sysconsole_write_compliance_data_retention_policy sysconsole_read_site_public_links manage_bots manage_system sysconsole_write_compliance_custom_terms_of_service playbook_public_manage_roles playbook_public_manage_properties playbook_private_create sysconsole_write_experimental_bleve sysconsole_read_authentication_email promote_guest get_saml_cert_status add_user_to_team sysconsole_write_site_users_and_teams create_custom_group manage_private_channel_members read_jobs sysconsole_write_experimental_features read_other_users_teams sysconsole_write_reporting_team_statistics sysconsole_read_environment_file_storage create_post_bleve_indexes_job sysconsole_read_site_file_sharing_and_downloads playbook_private_make_public playbook_public_view create_user_access_token create_public_channel read_channel sysconsole_read_user_management_channels sysconsole_read_user_management_permissions read_public_channel sysconsole_read_compliance_custom_terms_of_service sysconsole_write_site_emoji sysconsole_read_integrations_gif sysconsole_read_site_customization sysconsole_write_integrations_cors invite_user create_direct_channel sysconsole_write_user_management_teams run_create manage_custom_group_members read_ldap_sync_job sysconsole_read_site_notifications playbook_private_manage_properties sysconsole_read_integrations_bot_accounts convert_public_channel_to_private invalidate_email_invite reload_config get_saml_metadata_from_idp manage_secure_connections delete_private_channel sysconsole_read_about_edition_and_license convert_private_channel_to_public sysconsole_read_environment_developer recycle_database_connections remove_saml_private_cert manage_oauth sysconsole_write_environment_database sysconsole_write_site_notifications sysconsole_write_authentication_guest_access sysconsole_write_compliance_compliance_monitoring sysconsole_write_environment_image_proxy create_post_public manage_jobs remove_user_from_team delete_others_posts create_post_ephemeral playbook_private_view create_elasticsearch_post_aggregation_job remove_reaction add_reaction sysconsole_write_environment_high_availability sysconsole_write_authentication_openid sysconsole_write_user_management_permissions add_saml_idp_cert sysconsole_read_site_posts view_members sysconsole_write_environment_smtp sysconsole_read_authentication_saml create_post use_channel_mentions create_team playbook_private_manage_roles get_public_link sysconsole_write_billing manage_system_wide_oauth sysconsole_read_environment_database sysconsole_write_environment_session_lengths run_manage_properties sysconsole_write_authentication_saml sysconsole_read_environment_web_server sysconsole_read_environment_rate_limiting manage_public_channel_properties create_group_channel sysconsole_read_compliance_data_retention_policy sysconsole_read_environment_high_availability manage_others_slash_commands sysconsole_read_compliance_compliance_export delete_custom_group sysconsole_read_user_management_system_roles purge_elasticsearch_indexes view_team sysconsole_read_environment_performance_monitoring manage_channel_roles playbook_public_make_private remove_saml_public_cert demote_to_guest sysconsole_write_environment_performance_monitoring read_audits sysconsole_write_site_announcement_banner upload_file revoke_user_access_token read_others_bots test_email read_elasticsearch_post_aggregation_job sysconsole_read_compliance_compliance_monitoring join_private_teams delete_post sysconsole_write_site_public_links manage_team edit_custom_group sysconsole_write_experimental_feature_flags sysconsole_write_user_management_system_roles remove_others_reactions manage_license_information sysconsole_read_authentication_signup read_compliance_export_job sysconsole_write_environment_developer remove_saml_idp_cert manage_incoming_webhooks sysconsole_read_site_emoji assign_bot sysconsole_write_integrations_gif sysconsole_read_user_management_users delete_public_channel manage_outgoing_webhooks sysconsole_write_site_posts remove_ldap_private_cert sysconsole_write_site_file_sharing_and_downloads sysconsole_read_integrations_integration_management sysconsole_read_environment_logging test_site_url sysconsole_read_environment_session_lengths read_elasticsearch_post_indexing_job sysconsole_read_billing sysconsole_read_site_notices sysconsole_read_reporting_server_logs sysconsole_write_integrations_bot_accounts sysconsole_write_site_notices create_private_channel read_private_channel_groups run_view read_bots manage_roles test_s3 sysconsole_write_environment_push_notification_server get_logs invite_guest remove_ldap_public_cert sysconsole_read_environment_smtp',1,1); +INSERT INTO `Roles` VALUES ('kb6r9i58x7dxdb3srfohd66sse','system_admin','authentication.roles.global_admin.name','authentication.roles.global_admin.description',0,1662271986948,0,' list_public_teams edit_brand manage_private_channel_properties sysconsole_read_user_management_teams playbook_public_create manage_others_bots invalidate_caches manage_shared_channels sysconsole_write_environment_logging manage_others_outgoing_webhooks sysconsole_read_reporting_team_statistics sysconsole_read_plugins list_team_channels use_group_mentions sysconsole_read_site_users_and_teams sysconsole_write_site_localization get_analytics sysconsole_read_experimental_bleve manage_team_roles sysconsole_read_site_localization edit_post sysconsole_write_user_management_channels test_elasticsearch list_private_teams add_ldap_public_cert join_public_teams manage_slash_commands manage_others_incoming_webhooks manage_public_channel_members sysconsole_read_environment_elasticsearch sysconsole_write_site_customization delete_others_emojis run_manage_members create_emojis sysconsole_write_authentication_email sysconsole_write_compliance_compliance_export add_saml_private_cert create_bot sysconsole_write_environment_rate_limiting add_saml_public_cert edit_other_users sysconsole_write_integrations_integration_management read_user_access_token create_elasticsearch_post_indexing_job sysconsole_write_user_management_users assign_system_admin_role sysconsole_write_user_management_groups sysconsole_read_authentication_guest_access sysconsole_write_about_edition_and_license sysconsole_read_authentication_ldap sysconsole_read_experimental_feature_flags sysconsole_read_integrations_cors sysconsole_read_user_management_groups join_public_channels sysconsole_read_experimental_features test_ldap sysconsole_write_environment_elasticsearch sysconsole_write_reporting_server_logs sysconsole_read_environment_image_proxy sysconsole_read_site_announcement_banner sysconsole_read_reporting_site_statistics sysconsole_write_authentication_mfa sysconsole_read_authentication_openid purge_bleve_indexes playbook_public_manage_members delete_emojis sysconsole_write_environment_file_storage sysconsole_write_reporting_site_statistics playbook_private_manage_members import_team sysconsole_write_environment_web_server sysconsole_write_authentication_password read_public_channel_groups create_compliance_export_job sysconsole_read_authentication_password list_users_without_team sysconsole_read_authentication_mfa add_ldap_private_cert create_data_retention_job read_license_information sysconsole_write_authentication_signup sysconsole_read_environment_push_notification_server edit_others_posts download_compliance_export_result create_ldap_sync_job sysconsole_write_authentication_ldap sysconsole_write_plugins read_data_retention_job sysconsole_write_compliance_data_retention_policy sysconsole_read_site_public_links manage_bots manage_system sysconsole_write_compliance_custom_terms_of_service playbook_public_manage_roles playbook_public_manage_properties playbook_private_create sysconsole_write_experimental_bleve sysconsole_read_authentication_email promote_guest get_saml_cert_status add_user_to_team sysconsole_write_site_users_and_teams create_custom_group manage_private_channel_members read_jobs sysconsole_write_experimental_features read_other_users_teams sysconsole_write_reporting_team_statistics sysconsole_read_environment_file_storage create_post_bleve_indexes_job sysconsole_read_site_file_sharing_and_downloads playbook_private_make_public playbook_public_view create_user_access_token create_public_channel read_channel sysconsole_read_user_management_channels sysconsole_read_user_management_permissions read_public_channel sysconsole_read_compliance_custom_terms_of_service sysconsole_write_site_emoji sysconsole_read_integrations_gif sysconsole_read_site_customization sysconsole_write_integrations_cors invite_user create_direct_channel sysconsole_write_user_management_teams run_create manage_custom_group_members read_ldap_sync_job sysconsole_read_site_notifications playbook_private_manage_properties sysconsole_read_integrations_bot_accounts convert_public_channel_to_private invalidate_email_invite reload_config get_saml_metadata_from_idp manage_secure_connections delete_private_channel sysconsole_read_about_edition_and_license convert_private_channel_to_public sysconsole_read_environment_developer recycle_database_connections remove_saml_private_cert manage_oauth sysconsole_write_environment_database sysconsole_write_site_notifications sysconsole_write_authentication_guest_access sysconsole_write_compliance_compliance_monitoring sysconsole_write_environment_image_proxy create_post_public manage_jobs remove_user_from_team delete_others_posts create_post_ephemeral playbook_private_view create_elasticsearch_post_aggregation_job remove_reaction add_reaction sysconsole_write_environment_high_availability sysconsole_write_authentication_openid sysconsole_write_user_management_permissions add_saml_idp_cert sysconsole_read_site_posts view_members sysconsole_write_environment_smtp sysconsole_read_authentication_saml create_post use_channel_mentions create_team playbook_private_manage_roles get_public_link sysconsole_write_billing manage_system_wide_oauth sysconsole_read_environment_database sysconsole_write_environment_session_lengths run_manage_properties sysconsole_write_authentication_saml sysconsole_read_environment_web_server sysconsole_read_environment_rate_limiting manage_public_channel_properties create_group_channel sysconsole_read_compliance_data_retention_policy sysconsole_read_environment_high_availability manage_others_slash_commands sysconsole_read_compliance_compliance_export delete_custom_group sysconsole_read_user_management_system_roles purge_elasticsearch_indexes view_team sysconsole_read_environment_performance_monitoring manage_channel_roles playbook_public_make_private remove_saml_public_cert demote_to_guest sysconsole_write_environment_performance_monitoring read_audits sysconsole_write_site_announcement_banner upload_file revoke_user_access_token read_others_bots test_email read_elasticsearch_post_aggregation_job sysconsole_read_compliance_compliance_monitoring join_private_teams delete_post sysconsole_write_site_public_links manage_team edit_custom_group sysconsole_write_experimental_feature_flags sysconsole_write_user_management_system_roles remove_others_reactions manage_license_information sysconsole_read_authentication_signup read_compliance_export_job sysconsole_write_environment_developer remove_saml_idp_cert manage_incoming_webhooks sysconsole_read_site_emoji assign_bot sysconsole_write_integrations_gif sysconsole_read_user_management_users delete_public_channel manage_outgoing_webhooks sysconsole_write_site_posts remove_ldap_private_cert sysconsole_write_site_file_sharing_and_downloads sysconsole_read_integrations_integration_management sysconsole_read_environment_logging test_site_url sysconsole_read_environment_session_lengths read_elasticsearch_post_indexing_job sysconsole_read_billing sysconsole_read_site_notices sysconsole_read_reporting_server_logs sysconsole_write_integrations_bot_accounts sysconsole_write_site_notices create_private_channel read_private_channel_groups run_view read_bots manage_roles test_s3 sysconsole_write_environment_push_notification_server get_logs invite_guest remove_ldap_public_cert sysconsole_read_environment_smtp',1,1); INSERT INTO `Roles` VALUES ('km7kijhdtjbajquwu36uqneyoc','system_post_all','authentication.roles.system_post_all.name','authentication.roles.system_post_all.description',0,1662271986953,0,' create_post use_channel_mentions',0,1); INSERT INTO `Roles` VALUES ('no7s4436sjbzzqjpupg85mszty','custom_group_user','authentication.roles.custom_group_user.name','authentication.roles.custom_group_user.description',1662271985801,1662271986956,0,'',0,0); INSERT INTO `Roles` VALUES ('qo7e17c1m3rezyjqx5iq9dpmxe','system_manager','authentication.roles.system_manager.name','authentication.roles.system_manager.description',0,1662271986960,0,' sysconsole_write_environment_image_proxy sysconsole_read_environment_developer read_ldap_sync_job sysconsole_read_reporting_team_statistics recycle_database_connections get_logs read_private_channel_groups test_elasticsearch sysconsole_read_environment_logging purge_elasticsearch_indexes sysconsole_write_site_posts sysconsole_read_environment_database sysconsole_read_environment_performance_monitoring manage_team sysconsole_read_authentication_password sysconsole_write_site_users_and_teams sysconsole_read_user_management_channels sysconsole_write_environment_rate_limiting sysconsole_write_site_notifications read_license_information edit_brand sysconsole_read_plugins sysconsole_read_environment_high_availability sysconsole_read_environment_file_storage sysconsole_read_environment_elasticsearch sysconsole_write_environment_web_server sysconsole_write_environment_smtp sysconsole_write_environment_performance_monitoring sysconsole_write_environment_session_lengths sysconsole_write_user_management_groups convert_private_channel_to_public manage_private_channel_properties sysconsole_read_site_posts list_private_teams sysconsole_read_authentication_ldap sysconsole_read_authentication_guest_access sysconsole_read_site_emoji sysconsole_write_integrations_integration_management convert_public_channel_to_private manage_private_channel_members read_elasticsearch_post_aggregation_job manage_team_roles sysconsole_write_site_file_sharing_and_downloads read_channel read_public_channel sysconsole_read_authentication_openid add_user_to_team sysconsole_write_environment_developer sysconsole_write_site_localization sysconsole_read_about_edition_and_license test_s3 reload_config sysconsole_write_environment_elasticsearch test_site_url sysconsole_write_site_announcement_banner get_analytics sysconsole_read_environment_push_notification_server sysconsole_read_authentication_signup test_email sysconsole_write_integrations_bot_accounts sysconsole_write_integrations_cors view_team sysconsole_write_integrations_gif sysconsole_read_site_notices sysconsole_read_environment_image_proxy sysconsole_read_integrations_cors sysconsole_write_environment_push_notification_server join_public_teams test_ldap create_elasticsearch_post_aggregation_job sysconsole_read_environment_session_lengths sysconsole_write_environment_file_storage manage_public_channel_members sysconsole_write_site_customization sysconsole_read_site_announcement_banner sysconsole_read_environment_smtp sysconsole_write_user_management_teams delete_public_channel sysconsole_write_environment_logging read_public_channel_groups sysconsole_read_site_users_and_teams sysconsole_read_reporting_site_statistics sysconsole_read_site_localization sysconsole_read_site_customization sysconsole_read_environment_rate_limiting sysconsole_read_environment_web_server sysconsole_write_user_management_permissions sysconsole_read_site_file_sharing_and_downloads sysconsole_write_site_public_links sysconsole_read_site_public_links sysconsole_read_authentication_email read_elasticsearch_post_indexing_job sysconsole_read_authentication_saml remove_user_from_team delete_private_channel sysconsole_write_user_management_channels sysconsole_read_reporting_server_logs sysconsole_read_integrations_bot_accounts sysconsole_read_user_management_teams list_public_teams create_elasticsearch_post_indexing_job sysconsole_write_site_emoji invalidate_caches sysconsole_read_integrations_integration_management sysconsole_write_environment_high_availability sysconsole_read_user_management_permissions join_private_teams manage_channel_roles sysconsole_write_site_notices manage_public_channel_properties sysconsole_write_environment_database sysconsole_read_site_notifications sysconsole_read_user_management_groups sysconsole_read_integrations_gif sysconsole_read_authentication_mfa',0,1); INSERT INTO `Roles` VALUES ('rkr97ikkh7fixy86qsoo5rqm4c','system_user_access_token','authentication.roles.system_user_access_token.name','authentication.roles.system_user_access_token.description',0,1662271986965,0,' create_user_access_token read_user_access_token revoke_user_access_token',0,1); INSERT INTO `Roles` VALUES ('rxzdk5irm7rcffcfej9e33kqeo','team_user','authentication.roles.team_user.name','authentication.roles.team_user.description',0,1662271986968,0,' invite_user view_team read_public_channel playbook_public_create add_user_to_team playbook_private_create create_private_channel list_team_channels create_public_channel join_public_channels',1,1); -INSERT INTO `Roles` VALUES ('x768jnyzw3rkfx7xb66ehcac6o','channel_user','authentication.roles.channel_user.name','authentication.roles.channel_user.description',0,1662271986972,0,' manage_public_channel_properties create_post manage_private_channel_properties delete_public_channel manage_private_channel_members get_public_link delete_post delete_private_channel upload_file edit_post remove_reaction use_channel_mentions add_reaction read_channel use_slash_commands manage_public_channel_members',1,1); -INSERT INTO `Roles` VALUES ('ynn8aynsn7n1trtbuq6p4cyzhe','channel_guest','authentication.roles.channel_guest.name','authentication.roles.channel_guest.description',1605167829001,1662271986975,0,' read_channel add_reaction remove_reaction upload_file edit_post create_post use_channel_mentions use_slash_commands',1,1); +INSERT INTO `Roles` VALUES ('x768jnyzw3rkfx7xb66ehcac6o','channel_user','authentication.roles.channel_user.name','authentication.roles.channel_user.description',0,1662271986972,0,' manage_public_channel_properties create_post manage_private_channel_properties delete_public_channel manage_private_channel_members get_public_link delete_post delete_private_channel upload_file edit_post remove_reaction use_channel_mentions add_reaction read_channel manage_public_channel_members',1,1); +INSERT INTO `Roles` VALUES ('ynn8aynsn7n1trtbuq6p4cyzhe','channel_guest','authentication.roles.channel_guest.name','authentication.roles.channel_guest.description',1605167829001,1662271986975,0,' read_channel add_reaction remove_reaction upload_file edit_post create_post use_channel_mentions',1,1); INSERT INTO `Roles` VALUES ('yqyby79r9jggxg7a9dnenuawmo','run_member','authentication.roles.run_member.name','authentication.roles.run_member.description',1662271985813,1662271986979,0,' run_view',1,1); INSERT INTO `Roles` VALUES ('zzehkfnp67bg5g1owh6eptdcxc','system_user','authentication.roles.global_user.name','authentication.roles.global_user.description',0,1662271986983,0,' create_emojis join_public_teams list_public_teams edit_custom_group delete_emojis create_team create_group_channel manage_custom_group_members view_members delete_custom_group create_custom_group create_direct_channel',1,1); /*!40000 ALTER TABLE `Roles` ENABLE KEYS */; diff --git a/server/channels/testlib/testdata/postgres_migration_warmup.sql b/server/channels/testlib/testdata/postgres_migration_warmup.sql index 4dc1481c3a..b58b54e62e 100644 --- a/server/channels/testlib/testdata/postgres_migration_warmup.sql +++ b/server/channels/testlib/testdata/postgres_migration_warmup.sql @@ -17,7 +17,7 @@ SET client_encoding = 'UTF8'; INSERT INTO public.roles VALUES ('gkegg9mqi3rgbm9u444mnxkmbc', 'team_post_all_public', 'authentication.roles.team_post_all_public.name', 'authentication.roles.team_post_all_public.description', 0, 1662230812026, 0, ' create_post_public use_channel_mentions', false, true); INSERT INTO public.roles VALUES ('7ta1wfbacjy3zxid54n3cqjzqw', 'system_post_all_public', 'authentication.roles.system_post_all_public.name', 'authentication.roles.system_post_all_public.description', 0, 1662230812027, 0, ' create_post_public use_channel_mentions', false, true); INSERT INTO public.roles VALUES ('xf95ytghtjfsfd543dum68uzua', 'system_user_access_token', 'authentication.roles.system_user_access_token.name', 'authentication.roles.system_user_access_token.description', 0, 1662230812027, 0, ' create_user_access_token read_user_access_token revoke_user_access_token', false, true); -INSERT INTO public.roles VALUES ('nh5i9ik1u78hdcny9usdoixkuo', 'channel_user', 'authentication.roles.channel_user.name', 'authentication.roles.channel_user.description', 0, 1662230812029, 0, ' delete_post delete_public_channel use_channel_mentions manage_private_channel_properties manage_public_channel_properties delete_private_channel upload_file read_channel use_slash_commands get_public_link remove_reaction create_post add_reaction manage_private_channel_members edit_post manage_public_channel_members', true, true); +INSERT INTO public.roles VALUES ('nh5i9ik1u78hdcny9usdoixkuo', 'channel_user', 'authentication.roles.channel_user.name', 'authentication.roles.channel_user.description', 0, 1662230812029, 0, ' delete_post delete_public_channel use_channel_mentions manage_private_channel_properties manage_public_channel_properties delete_private_channel upload_file read_channel get_public_link remove_reaction create_post add_reaction manage_private_channel_members edit_post manage_public_channel_members', true, true); INSERT INTO public.roles VALUES ('peooyqpsq7g5bfnfo45zb1jiro', 'system_guest', 'authentication.roles.global_guest.name', 'authentication.roles.global_guest.description', 1605163387739, 1662230812021, 0, ' create_group_channel create_direct_channel', true, true); INSERT INTO public.roles VALUES ('96whs8mg73dszp7cz4u7sdbd7c', 'team_guest', 'authentication.roles.team_guest.name', 'authentication.roles.team_guest.description', 1605163387741, 1662230812022, 0, ' view_team', true, true); INSERT INTO public.roles VALUES ('rfc1w7z71pnzurkhpb1jgrbmdh', 'team_user', 'authentication.roles.team_user.name', 'authentication.roles.team_user.description', 1605163387747, 1662230812023, 0, ' playbook_public_create view_team invite_user playbook_private_create list_team_channels join_public_channels create_private_channel add_user_to_team read_public_channel create_public_channel', true, true); @@ -26,14 +26,14 @@ INSERT INTO public.roles VALUES ('wxat9mo53tg79xdzn55kdq148w', 'channel_admin', INSERT INTO public.roles VALUES ('13kpq8iaqffmdf9qkrfqmpby9h', 'team_admin', 'authentication.roles.team_admin.name', 'authentication.roles.team_admin.description', 0, 1662230812024, 0, ' manage_incoming_webhooks manage_others_incoming_webhooks import_team manage_others_outgoing_webhooks manage_team_roles remove_user_from_team manage_team manage_outgoing_webhooks manage_slash_commands convert_public_channel_to_private playbook_public_manage_roles manage_others_slash_commands delete_others_posts delete_post manage_channel_roles convert_private_channel_to_public playbook_private_manage_roles', true, true); INSERT INTO public.roles VALUES ('tj3atgnwjfrt7emz8pgqmh5z4c', 'team_post_all', 'authentication.roles.team_post_all.name', 'authentication.roles.team_post_all.description', 0, 1662230812030, 0, ' create_post use_channel_mentions', false, true); INSERT INTO public.roles VALUES ('d54xjt4sat8h7dqwu6i35jocuy', 'system_user', 'authentication.roles.global_user.name', 'authentication.roles.global_user.description', 0, 1662230812030, 0, ' create_emojis edit_custom_group manage_custom_group_members view_members create_custom_group create_team create_direct_channel delete_custom_group list_public_teams delete_emojis create_group_channel join_public_teams', true, true); -INSERT INTO public.roles VALUES ('mrejpofuoffiiynqcsi98es9ya', 'channel_guest', 'authentication.roles.channel_guest.name', 'authentication.roles.channel_guest.description', 0, 1662230812026, 0, ' upload_file edit_post create_post use_channel_mentions use_slash_commands read_channel add_reaction remove_reaction', true, true); +INSERT INTO public.roles VALUES ('mrejpofuoffiiynqcsi98es9ya', 'channel_guest', 'authentication.roles.channel_guest.name', 'authentication.roles.channel_guest.description', 0, 1662230812026, 0, ' upload_file edit_post create_post use_channel_mentions read_channel add_reaction remove_reaction', true, true); INSERT INTO public.roles VALUES ('4fk7nq4jgi8t7n1re79eb7i96c', 'custom_group_user', 'authentication.roles.custom_group_user.name', 'authentication.roles.custom_group_user.description', 1662230811506, 1662230812031, 0, '', false, false); INSERT INTO public.roles VALUES ('qmagi7t1ifbjuy5r1pp53eoryo', 'playbook_admin', 'authentication.roles.playbook_admin.name', 'authentication.roles.playbook_admin.description', 1662230811507, 1662230812032, 0, ' playbook_public_manage_roles playbook_public_manage_properties playbook_private_manage_members playbook_private_manage_roles playbook_private_manage_properties playbook_public_make_private playbook_public_manage_members', true, true); INSERT INTO public.roles VALUES ('ozgjpnirx7fdjp3i1i8jrg1kwc', 'system_custom_group_admin', 'authentication.roles.system_custom_group_admin.name', 'authentication.roles.system_custom_group_admin.description', 1662230811510, 1662230812032, 0, ' create_custom_group edit_custom_group delete_custom_group manage_custom_group_members', false, true); INSERT INTO public.roles VALUES ('pfnwpqmbmjrexgqbxdu61wfd3w', 'playbook_member', 'authentication.roles.playbook_member.name', 'authentication.roles.playbook_member.description', 1662230811533, 1662230812034, 0, ' playbook_public_view playbook_public_manage_members playbook_public_manage_properties playbook_private_view playbook_private_manage_members playbook_private_manage_properties run_create', true, true); INSERT INTO public.roles VALUES ('dj5zm9bxbidi9ritmana9t1sxh', 'run_admin', 'authentication.roles.run_admin.name', 'authentication.roles.run_admin.description', 1662230811534, 1662230812035, 0, ' run_manage_members run_manage_properties', true, true); INSERT INTO public.roles VALUES ('abrocgnx8pni7esbrmb4pjxhoe', 'run_member', 'authentication.roles.run_member.name', 'authentication.roles.run_member.description', 1662230811534, 1662230812036, 0, ' run_view', true, true); -INSERT INTO public.roles VALUES ('ha8u9qxwx3dm8mnbq8sfi7ugdc', 'system_admin', 'authentication.roles.global_admin.name', 'authentication.roles.global_admin.description', 0, 1662230812038, 0, ' read_public_channel_groups manage_public_channel_properties create_post_ephemeral sysconsole_write_site_localization sysconsole_write_billing sysconsole_read_site_file_sharing_and_downloads playbook_public_manage_roles sysconsole_read_integrations_gif delete_emojis sysconsole_write_experimental_features sysconsole_write_site_posts add_ldap_private_cert use_group_mentions sysconsole_read_authentication_openid add_user_to_team sysconsole_read_user_management_channels sysconsole_write_environment_high_availability sysconsole_write_site_announcement_banner sysconsole_read_site_notices sysconsole_write_user_management_teams convert_public_channel_to_private sysconsole_read_reporting_server_logs manage_system_wide_oauth revoke_user_access_token invalidate_caches sysconsole_write_environment_push_notification_server sysconsole_read_site_emoji remove_others_reactions sysconsole_write_reporting_server_logs sysconsole_write_user_management_permissions sysconsole_read_site_posts assign_bot sysconsole_write_authentication_password add_saml_private_cert manage_jobs sysconsole_write_environment_developer use_channel_mentions add_ldap_public_cert purge_bleve_indexes playbook_public_manage_properties sysconsole_read_authentication_mfa read_public_channel sysconsole_read_environment_image_proxy import_team sysconsole_read_reporting_team_statistics sysconsole_write_user_management_channels list_private_teams sysconsole_read_user_management_groups join_private_teams sysconsole_read_compliance_data_retention_policy list_public_teams sysconsole_read_site_localization sysconsole_write_authentication_guest_access sysconsole_read_compliance_compliance_monitoring sysconsole_read_environment_developer edit_others_posts sysconsole_read_experimental_bleve read_audits sysconsole_write_authentication_email sysconsole_write_experimental_bleve sysconsole_read_environment_push_notification_server read_elasticsearch_post_aggregation_job remove_ldap_private_cert manage_team manage_bots sysconsole_write_environment_session_lengths sysconsole_write_user_management_users sysconsole_write_environment_file_storage invite_user join_public_channels create_direct_channel sysconsole_read_site_users_and_teams manage_slash_commands playbook_public_view sysconsole_write_compliance_custom_terms_of_service purge_elasticsearch_indexes sysconsole_read_authentication_email test_ldap sysconsole_write_plugins manage_outgoing_webhooks create_bot create_compliance_export_job get_logs create_private_channel get_saml_metadata_from_idp read_elasticsearch_post_indexing_job get_analytics manage_incoming_webhooks sysconsole_read_authentication_saml invite_guest manage_shared_channels create_public_channel sysconsole_write_site_file_sharing_and_downloads sysconsole_read_environment_rate_limiting manage_public_channel_members sysconsole_read_environment_file_storage sysconsole_read_environment_performance_monitoring sysconsole_write_environment_performance_monitoring sysconsole_write_integrations_gif create_post_public playbook_public_manage_members upload_file sysconsole_write_reporting_team_statistics manage_team_roles sysconsole_read_site_notifications delete_public_channel sysconsole_write_compliance_compliance_monitoring create_ldap_sync_job create_data_retention_job sysconsole_write_environment_smtp manage_custom_group_members manage_others_slash_commands read_ldap_sync_job sysconsole_read_integrations_bot_accounts read_others_bots read_bots sysconsole_read_authentication_ldap demote_to_guest remove_saml_public_cert create_post_bleve_indexes_job sysconsole_read_user_management_teams sysconsole_write_about_edition_and_license remove_ldap_public_cert read_channel sysconsole_read_environment_database sysconsole_write_authentication_signup test_s3 sysconsole_read_environment_high_availability manage_roles sysconsole_write_site_notifications run_view sysconsole_write_authentication_saml invalidate_email_invite playbook_private_view read_compliance_export_job list_users_without_team sysconsole_read_compliance_compliance_export sysconsole_write_integrations_cors promote_guest manage_oauth read_data_retention_job sysconsole_write_experimental_feature_flags sysconsole_read_environment_session_lengths manage_license_information sysconsole_write_authentication_ldap assign_system_admin_role create_post read_private_channel_groups add_saml_idp_cert playbook_private_create manage_private_channel_properties sysconsole_read_compliance_custom_terms_of_service sysconsole_read_integrations_integration_management sysconsole_read_billing sysconsole_read_authentication_password delete_private_channel sysconsole_write_site_notices create_elasticsearch_post_indexing_job test_email sysconsole_write_environment_database recycle_database_connections edit_brand sysconsole_write_authentication_mfa remove_user_from_team sysconsole_write_user_management_system_roles add_reaction remove_saml_private_cert sysconsole_read_environment_web_server run_create sysconsole_read_authentication_guest_access sysconsole_read_about_edition_and_license run_manage_properties create_user_access_token manage_others_incoming_webhooks create_elasticsearch_post_aggregation_job sysconsole_write_user_management_groups sysconsole_read_experimental_feature_flags create_team sysconsole_read_environment_elasticsearch join_public_teams sysconsole_read_user_management_users sysconsole_read_integrations_cors sysconsole_read_environment_smtp manage_secure_connections manage_channel_roles edit_other_users delete_others_emojis sysconsole_write_site_users_and_teams add_saml_public_cert sysconsole_read_site_announcement_banner create_custom_group download_compliance_export_result create_group_channel get_saml_cert_status sysconsole_read_site_public_links manage_system create_emojis sysconsole_read_authentication_signup sysconsole_write_environment_image_proxy list_team_channels remove_saml_idp_cert sysconsole_read_plugins sysconsole_read_site_customization sysconsole_write_site_customization use_slash_commands playbook_private_manage_roles delete_custom_group delete_others_posts sysconsole_write_compliance_data_retention_policy sysconsole_write_environment_logging test_elasticsearch playbook_public_make_private sysconsole_write_site_public_links edit_post playbook_private_make_public sysconsole_write_environment_elasticsearch test_site_url sysconsole_write_compliance_compliance_export playbook_private_manage_members delete_post reload_config edit_custom_group sysconsole_read_user_management_system_roles sysconsole_write_reporting_site_statistics sysconsole_write_site_emoji read_user_access_token sysconsole_write_environment_rate_limiting view_members sysconsole_write_integrations_bot_accounts manage_others_bots manage_others_outgoing_webhooks sysconsole_read_environment_logging sysconsole_read_experimental_features sysconsole_write_authentication_openid manage_private_channel_members read_jobs sysconsole_write_environment_web_server read_license_information sysconsole_read_user_management_permissions view_team convert_private_channel_to_public sysconsole_read_reporting_site_statistics get_public_link read_other_users_teams sysconsole_write_integrations_integration_management run_manage_members playbook_public_create remove_reaction playbook_private_manage_properties', true, true); +INSERT INTO public.roles VALUES ('ha8u9qxwx3dm8mnbq8sfi7ugdc', 'system_admin', 'authentication.roles.global_admin.name', 'authentication.roles.global_admin.description', 0, 1662230812038, 0, ' read_public_channel_groups manage_public_channel_properties create_post_ephemeral sysconsole_write_site_localization sysconsole_write_billing sysconsole_read_site_file_sharing_and_downloads playbook_public_manage_roles sysconsole_read_integrations_gif delete_emojis sysconsole_write_experimental_features sysconsole_write_site_posts add_ldap_private_cert use_group_mentions sysconsole_read_authentication_openid add_user_to_team sysconsole_read_user_management_channels sysconsole_write_environment_high_availability sysconsole_write_site_announcement_banner sysconsole_read_site_notices sysconsole_write_user_management_teams convert_public_channel_to_private sysconsole_read_reporting_server_logs manage_system_wide_oauth revoke_user_access_token invalidate_caches sysconsole_write_environment_push_notification_server sysconsole_read_site_emoji remove_others_reactions sysconsole_write_reporting_server_logs sysconsole_write_user_management_permissions sysconsole_read_site_posts assign_bot sysconsole_write_authentication_password add_saml_private_cert manage_jobs sysconsole_write_environment_developer use_channel_mentions add_ldap_public_cert purge_bleve_indexes playbook_public_manage_properties sysconsole_read_authentication_mfa read_public_channel sysconsole_read_environment_image_proxy import_team sysconsole_read_reporting_team_statistics sysconsole_write_user_management_channels list_private_teams sysconsole_read_user_management_groups join_private_teams sysconsole_read_compliance_data_retention_policy list_public_teams sysconsole_read_site_localization sysconsole_write_authentication_guest_access sysconsole_read_compliance_compliance_monitoring sysconsole_read_environment_developer edit_others_posts sysconsole_read_experimental_bleve read_audits sysconsole_write_authentication_email sysconsole_write_experimental_bleve sysconsole_read_environment_push_notification_server read_elasticsearch_post_aggregation_job remove_ldap_private_cert manage_team manage_bots sysconsole_write_environment_session_lengths sysconsole_write_user_management_users sysconsole_write_environment_file_storage invite_user join_public_channels create_direct_channel sysconsole_read_site_users_and_teams manage_slash_commands playbook_public_view sysconsole_write_compliance_custom_terms_of_service purge_elasticsearch_indexes sysconsole_read_authentication_email test_ldap sysconsole_write_plugins manage_outgoing_webhooks create_bot create_compliance_export_job get_logs create_private_channel get_saml_metadata_from_idp read_elasticsearch_post_indexing_job get_analytics manage_incoming_webhooks sysconsole_read_authentication_saml invite_guest manage_shared_channels create_public_channel sysconsole_write_site_file_sharing_and_downloads sysconsole_read_environment_rate_limiting manage_public_channel_members sysconsole_read_environment_file_storage sysconsole_read_environment_performance_monitoring sysconsole_write_environment_performance_monitoring sysconsole_write_integrations_gif create_post_public playbook_public_manage_members upload_file sysconsole_write_reporting_team_statistics manage_team_roles sysconsole_read_site_notifications delete_public_channel sysconsole_write_compliance_compliance_monitoring create_ldap_sync_job create_data_retention_job sysconsole_write_environment_smtp manage_custom_group_members manage_others_slash_commands read_ldap_sync_job sysconsole_read_integrations_bot_accounts read_others_bots read_bots sysconsole_read_authentication_ldap demote_to_guest remove_saml_public_cert create_post_bleve_indexes_job sysconsole_read_user_management_teams sysconsole_write_about_edition_and_license remove_ldap_public_cert read_channel sysconsole_read_environment_database sysconsole_write_authentication_signup test_s3 sysconsole_read_environment_high_availability manage_roles sysconsole_write_site_notifications run_view sysconsole_write_authentication_saml invalidate_email_invite playbook_private_view read_compliance_export_job list_users_without_team sysconsole_read_compliance_compliance_export sysconsole_write_integrations_cors promote_guest manage_oauth read_data_retention_job sysconsole_write_experimental_feature_flags sysconsole_read_environment_session_lengths manage_license_information sysconsole_write_authentication_ldap assign_system_admin_role create_post read_private_channel_groups add_saml_idp_cert playbook_private_create manage_private_channel_properties sysconsole_read_compliance_custom_terms_of_service sysconsole_read_integrations_integration_management sysconsole_read_billing sysconsole_read_authentication_password delete_private_channel sysconsole_write_site_notices create_elasticsearch_post_indexing_job test_email sysconsole_write_environment_database recycle_database_connections edit_brand sysconsole_write_authentication_mfa remove_user_from_team sysconsole_write_user_management_system_roles add_reaction remove_saml_private_cert sysconsole_read_environment_web_server run_create sysconsole_read_authentication_guest_access sysconsole_read_about_edition_and_license run_manage_properties create_user_access_token manage_others_incoming_webhooks create_elasticsearch_post_aggregation_job sysconsole_write_user_management_groups sysconsole_read_experimental_feature_flags create_team sysconsole_read_environment_elasticsearch join_public_teams sysconsole_read_user_management_users sysconsole_read_integrations_cors sysconsole_read_environment_smtp manage_secure_connections manage_channel_roles edit_other_users delete_others_emojis sysconsole_write_site_users_and_teams add_saml_public_cert sysconsole_read_site_announcement_banner create_custom_group download_compliance_export_result create_group_channel get_saml_cert_status sysconsole_read_site_public_links manage_system create_emojis sysconsole_read_authentication_signup sysconsole_write_environment_image_proxy list_team_channels remove_saml_idp_cert sysconsole_read_plugins sysconsole_read_site_customization sysconsole_write_site_customization playbook_private_manage_roles delete_custom_group delete_others_posts sysconsole_write_compliance_data_retention_policy sysconsole_write_environment_logging test_elasticsearch playbook_public_make_private sysconsole_write_site_public_links edit_post playbook_private_make_public sysconsole_write_environment_elasticsearch test_site_url sysconsole_write_compliance_compliance_export playbook_private_manage_members delete_post reload_config edit_custom_group sysconsole_read_user_management_system_roles sysconsole_write_reporting_site_statistics sysconsole_write_site_emoji read_user_access_token sysconsole_write_environment_rate_limiting view_members sysconsole_write_integrations_bot_accounts manage_others_bots manage_others_outgoing_webhooks sysconsole_read_environment_logging sysconsole_read_experimental_features sysconsole_write_authentication_openid manage_private_channel_members read_jobs sysconsole_write_environment_web_server read_license_information sysconsole_read_user_management_permissions view_team convert_private_channel_to_public sysconsole_read_reporting_site_statistics get_public_link read_other_users_teams sysconsole_write_integrations_integration_management run_manage_members playbook_public_create remove_reaction playbook_private_manage_properties', true, true); INSERT INTO public.roles VALUES ('hm1bxei8b3d68e4j95tqnndppw', 'system_manager', 'authentication.roles.system_manager.name', 'authentication.roles.system_manager.description', 0, 1662230812025, 0, ' manage_private_channel_members join_public_teams sysconsole_write_site_announcement_banner sysconsole_write_site_emoji manage_public_channel_members purge_elasticsearch_indexes sysconsole_read_authentication_openid sysconsole_read_about_edition_and_license edit_brand sysconsole_read_reporting_team_statistics sysconsole_read_site_file_sharing_and_downloads sysconsole_read_user_management_teams read_private_channel_groups delete_public_channel sysconsole_read_site_customization sysconsole_write_site_notices sysconsole_read_authentication_email sysconsole_write_environment_file_storage sysconsole_read_user_management_permissions sysconsole_read_reporting_site_statistics test_s3 sysconsole_write_user_management_permissions sysconsole_read_environment_rate_limiting read_license_information sysconsole_read_environment_file_storage sysconsole_write_environment_elasticsearch invalidate_caches sysconsole_read_integrations_cors sysconsole_write_user_management_teams add_user_to_team sysconsole_read_environment_performance_monitoring get_logs sysconsole_write_environment_high_availability sysconsole_read_authentication_signup manage_public_channel_properties sysconsole_write_integrations_integration_management read_elasticsearch_post_indexing_job sysconsole_read_user_management_groups view_team sysconsole_write_environment_rate_limiting sysconsole_read_authentication_guest_access sysconsole_read_environment_elasticsearch manage_team reload_config manage_team_roles test_ldap sysconsole_read_site_public_links sysconsole_read_authentication_saml sysconsole_write_integrations_cors read_public_channel_groups sysconsole_write_site_users_and_teams sysconsole_read_integrations_gif get_analytics create_elasticsearch_post_indexing_job sysconsole_read_authentication_ldap sysconsole_read_site_announcement_banner test_site_url sysconsole_read_site_localization sysconsole_write_environment_push_notification_server sysconsole_write_integrations_bot_accounts sysconsole_write_environment_performance_monitoring sysconsole_write_site_posts sysconsole_read_environment_logging read_elasticsearch_post_aggregation_job sysconsole_write_site_localization sysconsole_write_environment_database sysconsole_read_site_posts sysconsole_write_environment_developer sysconsole_read_site_emoji sysconsole_read_plugins create_elasticsearch_post_aggregation_job manage_channel_roles sysconsole_write_user_management_groups remove_user_from_team read_ldap_sync_job sysconsole_write_site_notifications recycle_database_connections test_email sysconsole_read_site_notifications list_public_teams sysconsole_write_site_customization sysconsole_read_environment_smtp sysconsole_read_authentication_mfa sysconsole_read_integrations_integration_management sysconsole_read_user_management_channels sysconsole_read_reporting_server_logs sysconsole_write_site_public_links test_elasticsearch sysconsole_write_environment_smtp sysconsole_read_environment_push_notification_server sysconsole_write_environment_web_server sysconsole_write_environment_logging sysconsole_read_environment_session_lengths sysconsole_read_site_notices sysconsole_read_environment_high_availability join_private_teams sysconsole_read_authentication_password sysconsole_read_environment_developer delete_private_channel sysconsole_read_integrations_bot_accounts sysconsole_write_environment_session_lengths convert_private_channel_to_public sysconsole_read_environment_database sysconsole_read_environment_image_proxy convert_public_channel_to_private manage_private_channel_properties sysconsole_write_site_file_sharing_and_downloads read_public_channel list_private_teams sysconsole_write_integrations_gif sysconsole_read_environment_web_server sysconsole_read_site_users_and_teams sysconsole_write_user_management_channels read_channel sysconsole_write_environment_image_proxy', false, true); INSERT INTO public.roles VALUES ('f9drbz6cyjdmb8jof6smiqya7h', 'system_user_manager', 'authentication.roles.system_user_manager.name', 'authentication.roles.system_user_manager.description', 0, 1662230812028, 0, ' manage_team_roles sysconsole_read_authentication_saml manage_public_channel_members manage_channel_roles add_user_to_team sysconsole_read_authentication_ldap read_public_channel_groups join_public_teams convert_private_channel_to_public join_private_teams sysconsole_read_user_management_teams list_public_teams sysconsole_read_authentication_email list_private_teams sysconsole_read_authentication_signup read_public_channel sysconsole_read_authentication_mfa sysconsole_read_authentication_guest_access test_ldap manage_private_channel_members sysconsole_read_user_management_permissions read_channel remove_user_from_team delete_public_channel sysconsole_write_user_management_channels delete_private_channel sysconsole_read_authentication_openid sysconsole_write_user_management_teams manage_team sysconsole_read_user_management_groups view_team sysconsole_write_user_management_groups sysconsole_read_user_management_channels manage_public_channel_properties manage_private_channel_properties sysconsole_read_authentication_password read_ldap_sync_job convert_public_channel_to_private read_private_channel_groups', false, true); INSERT INTO public.roles VALUES ('tkioqq1sgtribqgjbzwop1846c', 'system_read_only_admin', 'authentication.roles.system_read_only_admin.name', 'authentication.roles.system_read_only_admin.description', 0, 1662230812033, 0, ' sysconsole_read_integrations_bot_accounts sysconsole_read_authentication_openid sysconsole_read_user_management_users sysconsole_read_authentication_saml read_ldap_sync_job read_other_users_teams sysconsole_read_user_management_permissions download_compliance_export_result sysconsole_read_environment_smtp sysconsole_read_site_localization read_public_channel read_audits sysconsole_read_compliance_custom_terms_of_service read_data_retention_job sysconsole_read_site_emoji sysconsole_read_compliance_data_retention_policy sysconsole_read_environment_developer sysconsole_read_site_file_sharing_and_downloads sysconsole_read_user_management_channels read_elasticsearch_post_indexing_job sysconsole_read_authentication_mfa sysconsole_read_compliance_compliance_monitoring sysconsole_read_authentication_signup sysconsole_read_authentication_ldap sysconsole_read_authentication_password get_analytics sysconsole_read_site_posts sysconsole_read_environment_performance_monitoring sysconsole_read_compliance_compliance_export sysconsole_read_integrations_integration_management test_ldap sysconsole_read_environment_file_storage sysconsole_read_environment_logging sysconsole_read_user_management_groups sysconsole_read_environment_high_availability sysconsole_read_environment_database sysconsole_read_environment_elasticsearch sysconsole_read_environment_push_notification_server sysconsole_read_site_notices read_compliance_export_job read_license_information sysconsole_read_environment_session_lengths read_private_channel_groups sysconsole_read_integrations_gif read_elasticsearch_post_aggregation_job sysconsole_read_experimental_bleve sysconsole_read_reporting_team_statistics sysconsole_read_about_edition_and_license sysconsole_read_environment_image_proxy sysconsole_read_site_customization sysconsole_read_environment_rate_limiting view_team sysconsole_read_site_announcement_banner sysconsole_read_environment_web_server get_logs sysconsole_read_experimental_feature_flags sysconsole_read_integrations_cors sysconsole_read_authentication_guest_access sysconsole_read_plugins read_channel list_public_teams sysconsole_read_user_management_teams sysconsole_read_reporting_server_logs sysconsole_read_experimental_features sysconsole_read_authentication_email sysconsole_read_site_notifications sysconsole_read_site_users_and_teams sysconsole_read_reporting_site_statistics read_public_channel_groups list_private_teams sysconsole_read_site_public_links', false, true); diff --git a/server/model/permission.go b/server/model/permission.go index 231154e2d4..e91809127e 100644 --- a/server/model/permission.go +++ b/server/model/permission.go @@ -21,10 +21,6 @@ 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 var PermissionCreatePublicChannel *Permission @@ -393,12 +389,6 @@ func initializePermissions() { "authentication.permissions.add_user_to_team.description", PermissionScopeTeam, } - PermissionUseSlashCommands = &Permission{ - "use_slash_commands", - "authentication.permissions.team_use_slash_commands.name", - "authentication.permissions.team_use_slash_commands.description", - PermissionScopeChannel, - } PermissionManageSlashCommands = &Permission{ "manage_slash_commands", "authentication.permissions.manage_slash_commands.name", @@ -2318,7 +2308,6 @@ func initializePermissions() { } ChannelScopedPermissions := []*Permission{ - PermissionUseSlashCommands, PermissionManagePublicChannelMembers, PermissionManagePrivateChannelMembers, PermissionManageChannelRoles, diff --git a/server/model/role.go b/server/model/role.go index 2c7a8fbf7b..4fba0c64f7 100644 --- a/server/model/role.go +++ b/server/model/role.go @@ -755,7 +755,6 @@ func MakeDefaultRoles() map[string]*Role { PermissionEditPost.Id, PermissionCreatePost.Id, PermissionUseChannelMentions.Id, - PermissionUseSlashCommands.Id, }, SchemeManaged: true, BuiltIn: true, @@ -774,7 +773,6 @@ func MakeDefaultRoles() map[string]*Role { PermissionGetPublicLink.Id, PermissionCreatePost.Id, PermissionUseChannelMentions.Id, - PermissionUseSlashCommands.Id, PermissionManagePublicChannelProperties.Id, PermissionDeletePublicChannel.Id, PermissionManagePrivateChannelProperties.Id, diff --git a/server/model/role_test.go b/server/model/role_test.go index 431a3286f1..d6142841dc 100644 --- a/server/model/role_test.go +++ b/server/model/role_test.go @@ -71,7 +71,6 @@ func TestRolePatchFromChannelModerationsPatch(t *testing.T) { PermissionManagePublicChannelMembers.Id, PermissionUploadFile.Id, PermissionGetPublicLink.Id, - PermissionUseSlashCommands.Id, } baseModeratedPermissions := []string{ diff --git a/server/model/utils.go b/server/model/utils.go index a46bddabae..956aa8caf3 100644 --- a/server/model/utils.go +++ b/server/model/utils.go @@ -251,6 +251,8 @@ type AppError struct { wrapped error } +const maxErrorLength = 1024 + func (er *AppError) Error() string { var sb strings.Builder @@ -276,7 +278,11 @@ func (er *AppError) Error() string { sb.WriteString(err.Error()) } - return sb.String() + res := sb.String() + if len(res) > maxErrorLength { + res = res[:maxErrorLength] + "..." + } + return res } func (er *AppError) Translate(T i18n.TranslateFunc) { diff --git a/server/model/utils_test.go b/server/model/utils_test.go index 606477d750..7b5e099e96 100644 --- a/server/model/utils_test.go +++ b/server/model/utils_test.go @@ -116,6 +116,13 @@ func TestAppErrorRender(t *testing.T) { aerr := NewAppError("here", "message", nil, "details", http.StatusTeapot).Wrap(fmt.Errorf("my error (%w)", fmt.Errorf("inner error"))) assert.EqualError(t, aerr, "here: message, details, my error (inner error)") }) + + t.Run("MaxLength", func(t *testing.T) { + str := strings.Repeat("error", 65536) + msg := "msg" + aerr := NewAppError("id", msg, nil, str, http.StatusTeapot).Wrap(errors.New(str)) + assert.Len(t, aerr.Error(), maxErrorLength+len(msg)) + }) } func TestAppErrorSerialize(t *testing.T) { diff --git a/server/model/worktemplate.go b/server/model/worktemplate.go index b0c4262784..73857524bb 100644 --- a/server/model/worktemplate.go +++ b/server/model/worktemplate.go @@ -69,7 +69,8 @@ type WorkTemplatePlaybook struct { } type WorkTemplateIntegration struct { - ID string `json:"id"` + ID string `json:"id"` + Recommended bool `json:"recommended"` } type WorkTemplateContent struct { diff --git a/server/platform/shared/markdown/inlines.go b/server/platform/shared/markdown/inlines.go index 43dee3bd32..973ae5ed21 100644 --- a/server/platform/shared/markdown/inlines.go +++ b/server/platform/shared/markdown/inlines.go @@ -628,7 +628,7 @@ func MergeInlineText(inlines []Inline) []Inline { } func Unescape(markdown string) string { - ret := "" + var ret strings.Builder position := 0 for position < len(markdown) { @@ -637,27 +637,27 @@ func Unescape(markdown string) string { switch c { case '\\': if position+1 < len(markdown) && isEscapableByte(markdown[position+1]) { - ret += string(markdown[position+1]) + ret.WriteByte(markdown[position+1]) position += 2 } else { - ret += `\` + ret.WriteString(`\`) position++ } case '&': position++ if semicolon := strings.IndexByte(markdown[position:], ';'); semicolon == -1 { - ret += "&" + ret.WriteString("&") } else if s := CharacterReference(markdown[position : position+semicolon]); s != "" { position += semicolon + 1 - ret += s + ret.WriteString(s) } else { - ret += "&" + ret.WriteString("&") } default: - ret += string(c) + ret.WriteRune(c) position += cSize } } - return ret + return ret.String() } diff --git a/server/playbooks/server/api/api.yaml b/server/playbooks/server/api/api.yaml index 538c03ca79..bed383530c 100644 --- a/server/playbooks/server/api/api.yaml +++ b/server/playbooks/server/api/api.yaml @@ -10,7 +10,7 @@ info: servers: - url: http://localhost:8065/plugins/playbooks/api/v0 paths: - /runs: + /plugins/playbooks/api/v0/runs: get: summary: List all playbook runs description: Retrieve a paged list of playbook runs, filtered by team, status, owner, name and/or members, and sorted by ID, name, status, creation date, end date, team or owner ID. @@ -198,7 +198,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/dialog: + /plugins/playbooks/api/v0/runs/dialog: post: summary: Create a new playbook run from dialog description: This is an internal endpoint to create a playbook run from the submission of an interactive dialog, filled by a user in the webapp. See [Interactive Dialogs](https://docs.mattermost.com/developer/interactive-dialogs.html) for more information. @@ -276,7 +276,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/owners: + /plugins/playbooks/api/v0/runs/owners: get: summary: Get all owners description: Get the owners of all playbook runs, filtered by team. @@ -314,7 +314,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/channels: + /plugins/playbooks/api/v0/runs/channels: get: summary: Get playbook run channels description: Get all channels associated with a playbook run, filtered by team, status, owner, name and/or members, and sorted by ID, name, status, creation date, end date, team, or owner ID. @@ -413,7 +413,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/checklist-autocomplete: + /plugins/playbooks/api/v0/runs/checklist-autocomplete: get: summary: Get autocomplete data for /playbook check description: This is an internal endpoint used by the autocomplete system to retrieve the data needed to show the list of items that the user can check. @@ -459,7 +459,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/channel/{channel_id}: + /plugins/playbooks/api/v0/runs/channel/{channel_id}: get: summary: Find playbook run by channel ID operationId: getPlaybookRunByChannelId @@ -492,7 +492,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}: + /plugins/playbooks/api/v0/runs/{id}: get: summary: Get a playbook run operationId: getPlaybookRun @@ -565,7 +565,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/metadata: + /plugins/playbooks/api/v0/runs/{id}/metadata: get: summary: Get playbook run metadata operationId: getPlaybookRunMetadata @@ -598,7 +598,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/end: + /plugins/playbooks/api/v0/runs/{id}/end: put: summary: End a playbook run operationId: endPlaybookRun @@ -651,7 +651,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/restart: + /plugins/playbooks/api/v0/runs/{id}/restart: put: summary: Restart a playbook run operationId: restartPlaybookRun @@ -678,7 +678,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/status: + /plugins/playbooks/api/v0/runs/{id}/status: post: summary: Update a playbook run's status operationId: status @@ -728,7 +728,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/finish: + /plugins/playbooks/api/v0/runs/{id}/finish: put: summary: Finish a playbook operationId: finish @@ -755,7 +755,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/owner: + /plugins/playbooks/api/v0/runs/{id}/owner: post: summary: Update playbook run owner operationId: changeOwner @@ -800,7 +800,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/next-stage-dialog: + /plugins/playbooks/api/v0/runs/{id}/next-stage-dialog: post: summary: Go to next stage from dialog description: This is an internal endpoint to go to the next stage via a confirmation dialog, submitted by a user in the webapp. @@ -835,7 +835,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/checklists/{checklist}/add: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/add: put: summary: Add an item to a playbook run's checklist description: The most common pattern to add a new item is to only send its title as the request payload. By default, it is an open item, with no assignee and no slash command. @@ -923,7 +923,7 @@ paths: schema: $ref: "#/components/schemas/Error" - /runs/{id}/checklists/{checklist}/reorder: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/reorder: put: summary: Reorder an item in a playbook run's checklist operationId: reoderChecklistItem @@ -978,7 +978,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/checklists/{checklist}/item/{item}: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}: put: summary: Update an item of a playbook run's checklist description: Update the title and the slash command of an item in one of the playbook run's checklists. @@ -1083,7 +1083,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/checklists/{checklist}/item/{item}/state: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}/state: put: summary: Update the state of an item operationId: itemSetState @@ -1145,7 +1145,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/checklists/{checklist}/item/{item}/assignee: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}/assignee: put: summary: Update the assignee of an item operationId: itemSetAssignee @@ -1202,7 +1202,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/checklists/{checklist}/item/{item}/run: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}/run: put: summary: Run an item's slash command operationId: itemRun @@ -1249,7 +1249,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/timeline/{event_id}/: + /plugins/playbooks/api/v0/runs/{id}/timeline/{event_id}/: delete: summary: Remove a timeline event from the playbook run operationId: removeTimelineEvent @@ -1285,7 +1285,7 @@ paths: 500: $ref: "#/components/responses/500" - /playbooks: + /plugins/playbooks/api/v0/playbooks: get: summary: List all playbooks description: Retrieve a paged list of playbooks, filtered by team, and sorted by title, number of stages or number of steps. @@ -1562,7 +1562,7 @@ paths: 500: $ref: "#/components/responses/500" - /playbooks/{id}: + /plugins/playbooks/api/v0/playbooks/{id}: get: summary: Get a playbook operationId: getPlaybook @@ -1658,7 +1658,7 @@ paths: 500: $ref: "#/components/responses/500" - /playbooks/{id}/autofollows: + /plugins/playbooks/api/v0/playbooks/{id}/autofollows: get: summary: Get the list of followers' user IDs of a playbook operationId: getAutoFollows diff --git a/server/playbooks/server/api/playbooks.go b/server/playbooks/server/api/playbooks.go index af9f60a459..6a87d5a74c 100644 --- a/server/playbooks/server/api/playbooks.go +++ b/server/playbooks/server/api/playbooks.go @@ -10,14 +10,12 @@ import ( "net/url" "strconv" "strings" - "time" "github.com/gorilla/mux" "github.com/mattermost/mattermost-server/server/v8/model" "github.com/mattermost/mattermost-server/server/v8/playbooks/server/app" "github.com/mattermost/mattermost-server/server/v8/playbooks/server/config" "github.com/mattermost/mattermost-server/server/v8/playbooks/server/playbooks" - "github.com/mattermost/mattermost-server/server/v8/playbooks/server/timeutils" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -693,14 +691,8 @@ func (h *PlaybookHandler) getTopPlaybooksForUser(c *Context, w http.ResponseWrit h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user", err) return } - timezone, err := timeutils.GetUserTimezone(user) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user timezone", err) - return - } - if timezone == nil { - timezone = time.Now().UTC().Location() - } + timezone := user.GetTimezoneLocation() + // get unix time for duration startTime, appErr := model.GetStartOfDayForTimeRange(timeRange, timezone) if appErr != nil { @@ -750,14 +742,8 @@ func (h *PlaybookHandler) getTopPlaybooksForTeam(c *Context, w http.ResponseWrit h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user", err) return } - timezone, err := timeutils.GetUserTimezone(user) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user timezone", err) - return - } - if timezone == nil { - timezone = time.Now().UTC().Location() - } + timezone := user.GetTimezoneLocation() + // get unix time for duration startTime, appErr := model.GetStartOfDayForTimeRange(timeRange, timezone) if appErr != nil { diff --git a/webapp/boards/src/blocks/attachmentBlock.tsx b/webapp/boards/src/blocks/attachmentBlock.tsx index bf0568505a..f84e6d051a 100644 --- a/webapp/boards/src/blocks/attachmentBlock.tsx +++ b/webapp/boards/src/blocks/attachmentBlock.tsx @@ -3,7 +3,7 @@ import {Block, createBlock} from './block' type AttachmentBlockFields = { - attachmentId: string + fileId: string } type AttachmentBlock = Block & { @@ -18,7 +18,7 @@ function createAttachmentBlock(block?: Block): AttachmentBlock { ...createBlock(block), type: 'attachment', fields: { - attachmentId: block?.fields.attachmentId || '', + fileId: block?.fields.attachmentId || block?.fields.fileId || '', }, isUploading: false, uploadingPercent: 0, diff --git a/webapp/boards/src/components/cardDialog.tsx b/webapp/boards/src/components/cardDialog.tsx index 7b8be86c20..b1187db750 100644 --- a/webapp/boards/src/components/cardDialog.tsx +++ b/webapp/boards/src/components/cardDialog.tsx @@ -151,7 +151,7 @@ const CardDialog = (props: Props): JSX.Element => { Utils.selectLocalFile(async (attachment) => { const uploadingBlock = createBlock() uploadingBlock.title = attachment.name - uploadingBlock.fields.attachmentId = attachment.name + uploadingBlock.fields.fileId = attachment.name uploadingBlock.boardId = boardId if (card) { uploadingBlock.parentId = card.id @@ -177,11 +177,11 @@ const CardDialog = (props: Props): JSX.Element => { xhr.onload = () => { if (xhr.status === 200 && xhr.readyState === 4) { const json = JSON.parse(xhr.response) - const attachmentId = json.fileId - if (attachmentId) { + const fileId = json.fileId + if (fileId) { removeUploadingAttachment(uploadingBlock) const block = createAttachmentBlock() - block.fields.attachmentId = attachmentId || '' + block.fields.fileId = fileId || '' block.title = attachment.name sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.uploadSuccess', defaultMessage: 'Attachment uploaded.'}), severity: 'normal'}) resolve(block) diff --git a/webapp/boards/src/components/content/attachmentElement.test.tsx b/webapp/boards/src/components/content/attachmentElement.test.tsx index 80a8ac6d77..1892368e62 100644 --- a/webapp/boards/src/components/content/attachmentElement.test.tsx +++ b/webapp/boards/src/components/content/attachmentElement.test.tsx @@ -39,7 +39,7 @@ describe('component/content/FileBlock', () => { type: 'attachment', title: 'test-title', fields: { - attachmentId: 'test.txt', + fileId: 'test.txt', }, createdBy: 'test-user-id', createAt: 0, diff --git a/webapp/boards/src/components/content/attachmentElement.tsx b/webapp/boards/src/components/content/attachmentElement.tsx index 1d18bf442b..58efbdce9b 100644 --- a/webapp/boards/src/components/content/attachmentElement.tsx +++ b/webapp/boards/src/components/content/attachmentElement.tsx @@ -50,7 +50,7 @@ const AttachmentElement = (props: Props): JSX.Element|null => { }) return } - const attachmentInfo = await octoClient.getFileInfo(block.boardId, block.fields.attachmentId) + const attachmentInfo = await octoClient.getFileInfo(block.boardId, block.fields.fileId) setFileInfo(attachmentInfo) } loadFile() @@ -113,7 +113,7 @@ const AttachmentElement = (props: Props): JSX.Element|null => { } const attachmentDownloadHandler = async () => { - const attachment = await octoClient.getFileAsDataUrl(block.boardId, block.fields.attachmentId) + const attachment = await octoClient.getFileAsDataUrl(block.boardId, block.fields.fileId) const anchor = document.createElement('a') anchor.href = attachment.url || '' anchor.download = fileInfo.name || '' diff --git a/webapp/boards/src/components/sidebar/sidebarCategory.tsx b/webapp/boards/src/components/sidebar/sidebarCategory.tsx index 5b54c8c7a9..734490c9ed 100644 --- a/webapp/boards/src/components/sidebar/sidebarCategory.tsx +++ b/webapp/boards/src/components/sidebar/sidebarCategory.tsx @@ -25,6 +25,7 @@ import CompassIcon from 'src/widgets/icons/compassIcon' import OptionsIcon from 'src/widgets/icons/options' import Menu from 'src/widgets/menu' import MenuWrapper from 'src/widgets/menuWrapper' +import {UserSettings} from 'src/userSettings' import './sidebarCategory.scss' import {Category, CategoryBoardMetadata, CategoryBoards} from 'src/store/sidebar' @@ -202,12 +203,24 @@ const SidebarCategory = (props: Props) => { setTimeout(() => { showBoard(props.boards[nextBoardId as number].id) }, 120) + } else { + setTimeout(() => { + const newPath = generatePath('/team/:teamId', {teamId: teamID,}) + history.push(newPath) + }, 120) } }, async () => { showBoard(deleteBoard.id) }, ) + if ( + UserSettings.lastBoardId && + UserSettings.lastBoardId[deleteBoard.teamId] == deleteBoard.id + ) { + UserSettings.setLastBoardID(deleteBoard.teamId, null) + UserSettings.setLastViewId(deleteBoard.id, null) + } }, [showBoard, deleteBoard, props.boards]) const updateCategory = useCallback(async (value: boolean) => { diff --git a/webapp/boards/src/pages/boardPage/boardPage.tsx b/webapp/boards/src/pages/boardPage/boardPage.tsx index 4f7fc321ba..1bce774cb7 100644 --- a/webapp/boards/src/pages/boardPage/boardPage.tsx +++ b/webapp/boards/src/pages/boardPage/boardPage.tsx @@ -186,7 +186,9 @@ const BoardPage = (props: Props): JSX.Element => { const joinBoard = async (myUser: IUser, boardTeamId: string, boardId: string, allowAdmin: boolean) => { const member = await octoClient.joinBoard(boardId, allowAdmin) if (!member) { - if (myUser.permissions?.find((s) => s === 'manage_system' || s === 'manage_team')) { + // if allowAdmin is true, then we failed to join the board + // as an admin, normally, this is deleted/missing board + if (!allowAdmin && myUser.permissions?.find((s) => s === 'manage_system' || s === 'manage_team')) { setShowJoinBoardDialog(true) return } diff --git a/webapp/channels/src/actions/global_actions.tsx b/webapp/channels/src/actions/global_actions.tsx index 6ac09280ab..b658499c4c 100644 --- a/webapp/channels/src/actions/global_actions.tsx +++ b/webapp/channels/src/actions/global_actions.tsx @@ -12,8 +12,8 @@ import { import {logout, loadMe, loadMeREST} from 'mattermost-redux/actions/users'; import {Preferences} from 'mattermost-redux/constants'; import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general'; -import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; -import {getBool, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships, getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams'; +import {getBool, getIsOnboardingFlowEnabled, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUser, getCurrentUserId, isFirstAdmin} from 'mattermost-redux/selectors/entities/users'; import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels'; import {appsEnabled} from 'mattermost-redux/selectors/entities/apps'; @@ -352,7 +352,7 @@ export async function redirectUserToDefaultTeam() { // Assume we need to load the user if they don't have any team memberships loaded or the user loaded let user = getCurrentUser(state); const shouldLoadUser = Utils.isEmptyObject(getTeamMemberships(state)) || !user; - + const onboardingFlowEnabled = getIsOnboardingFlowEnabled(state); if (shouldLoadUser) { if (isGraphQLEnabled(state)) { await dispatch(loadMe()); @@ -374,8 +374,9 @@ export async function redirectUserToDefaultTeam() { const teamId = LocalStorageStore.getPreviousTeamId(user.id); let myTeams = getMyTeams(state); - if (myTeams.length === 0) { - if (isUserFirstAdmin) { + const teams = getActiveTeamsList(state); + if (teams.length === 0) { + if (isUserFirstAdmin && onboardingFlowEnabled) { getHistory().push('/preparing-workspace'); return; } diff --git a/webapp/channels/src/components/activity_and_insights/insights/insights.tsx b/webapp/channels/src/components/activity_and_insights/insights/insights.tsx index 50ca7b71a6..736e63769d 100644 --- a/webapp/channels/src/components/activity_and_insights/insights/insights.tsx +++ b/webapp/channels/src/components/activity_and_insights/insights/insights.tsx @@ -16,7 +16,6 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {selectLhsItem} from 'actions/views/lhs'; -import {GlobalState} from 'types/store'; import {LhsItemType, LhsPage} from 'types/store/lhs'; import {CardSizes, InsightsWidgetTypes, TimeFrame, TimeFrames} from '@mattermost/types/insights'; @@ -41,17 +40,20 @@ type SelectOption = { const Insights = () => { const dispatch = useDispatch(); - - // check if either of focalboard plugin or boards product is enabled - const focalboardPluginEnabled = useSelector((state: GlobalState) => state.plugins.plugins?.focalboard); - let focalboardProductEnabled = false; const products = useProducts(); - if (products) { - focalboardProductEnabled = products.some((product) => product.pluginId === suitePluginIds.focalboard || product.pluginId === suitePluginIds.boards); - } - const focalboardEnabled = focalboardPluginEnabled || focalboardProductEnabled; - const playbooksEnabled = useSelector((state: GlobalState) => state.plugins.plugins?.playbooks); + let focalboardEnabled = false; + let playbooksEnabled = false; + if (products) { + products.forEach((product) => { + if (product.pluginId === suitePluginIds.boards) { + focalboardEnabled = true; + } else if (product.pluginId === suitePluginIds.playbooks) { + playbooksEnabled = true; + } + }); + } + const currentUserId = useSelector(getCurrentUserId); const currentTeamId = useSelector(getCurrentTeamId); diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx index 3e5bec63e9..fd42f863a1 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx @@ -59,13 +59,13 @@ const ContactSalesCard = (props: Props) => { title = ( ); description = ( ); } else { @@ -103,13 +103,13 @@ const ContactSalesCard = (props: Props) => { title = ( ); description = ( ); break; diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx index e5640663b0..cdfb950a63 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx @@ -163,7 +163,7 @@ const ToYearlyNudgeBannerDismissable = () => { type={announcementType} showCloseButton={daysToProMonthlyEnd > 10} onButtonClick={() => openPurchaseModal({trackingLocation: 'to_yearly_nudge_annoucement_bar'})} - modalButtonText={t('cloud_billing.nudge_to_yearly.learn_more')} + modalButtonText={t('cloud_billing.nudge_to_yearly.update_billing')} modalButtonDefaultText='Update billing' message={message} showLinkAsButton={true} diff --git a/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx b/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx index 5a3c40872a..a8e6b6ccbd 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx @@ -55,7 +55,7 @@ export const noBillingHistory = ( ); -export const freeTrial = (onUpgradeMattermostCloud: (callerInfo: string) => void, daysLeftOnTrial: number) => ( +export const freeTrial = (onUpgradeMattermostCloud: (callerInfo: string) => void, daysLeftOnTrial: number, reverseTrial: boolean) => (
void onClick={() => onUpgradeMattermostCloud('billing_summary_free_trial_upgrade_button')} className='UpgradeMattermostCloud__upgradeButton' > - + { + reverseTrial ? ( + + + ) : ( + + ) + + }
); diff --git a/webapp/channels/src/components/admin_console/billing/billing_summary/index.tsx b/webapp/channels/src/components/admin_console/billing/billing_summary/index.tsx index e53bd72570..e4e3e1a2ce 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_summary/index.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_summary/index.tsx @@ -5,6 +5,7 @@ import React from 'react'; import {useSelector} from 'react-redux'; import {getSubscriptionProduct, checkHadPriorTrial, getCloudSubscription} from 'mattermost-redux/selectors/entities/cloud'; +import {cloudReverseTrial} from 'mattermost-redux/selectors/entities/preferences'; import {CloudProducts} from 'utils/constants'; @@ -27,20 +28,23 @@ type BillingSummaryProps = { const BillingSummary = ({isFreeTrial, daysLeftOnTrial, onUpgradeMattermostCloud}: BillingSummaryProps) => { const subscription = useSelector(getCloudSubscription); const product = useSelector(getSubscriptionProduct); + const reverseTrial = useSelector(cloudReverseTrial); let body = noBillingHistory; const isPreTrial = subscription?.is_free_trial === 'false' && subscription?.trial_end_at === 0; const hasPriorTrial = useSelector(checkHadPriorTrial); - const showTryEnterprise = product?.sku === CloudProducts.STARTER && isPreTrial; - const showUpgradeProfessional = product?.sku === CloudProducts.STARTER && hasPriorTrial; + const isStarterPreTrial = product?.sku === CloudProducts.STARTER && isPreTrial; + const isStarterPostTrial = product?.sku === CloudProducts.STARTER && hasPriorTrial; - if (showTryEnterprise) { + if (isStarterPreTrial && reverseTrial) { + body = ; + } else if (isStarterPreTrial) { body = tryEnterpriseCard; - } else if (showUpgradeProfessional) { + } else if (isStarterPostTrial) { body = ; } else if (isFreeTrial) { - body = freeTrial(onUpgradeMattermostCloud, daysLeftOnTrial); + body = freeTrial(onUpgradeMattermostCloud, daysLeftOnTrial, reverseTrial); } else if (subscription?.last_invoice && !subscription?.upcoming_invoice) { const invoice = subscription.last_invoice; const fullCharges = invoice.line_items.filter((item) => item.type === 'full'); diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss index 2bcbbebd2a..514dd90e0a 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss @@ -12,6 +12,10 @@ } } + &__Icon { + padding-top: 8px; + } + &__Title { color: var(--sys-denim-center-channel-text); font-family: Metropolis; @@ -21,15 +25,17 @@ } &__Usage { + color: var(--center-channel-color); text-align: left; &-Highlighted { color: black; - font-weight: 700; + font-weight: bold; } } &__Warning { + color: var(--center-channel-color); text-align: left; } diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx index 5fc7c7c2a7..f41cc7b716 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx @@ -186,8 +186,8 @@ export default function DeleteWorkspaceModal(props: Props) { className='DeleteWorkspaceModal' onExited={handleClickCancel} > -
- +
+
{ hadPrevCloudTrial={false} isSubscriptionLoaded={true} isPaidSubscription={false} + cloudFreeDeprecated={false} actions={{ getPrevTrialLicense: jest.fn(), getCloudSubscription: jest.fn(), @@ -58,6 +59,7 @@ describe('components/feature_discovery', () => { isCloudTrial={false} hadPrevCloudTrial={false} isPaidSubscription={false} + cloudFreeDeprecated={false} isSubscriptionLoaded={true} actions={{ getPrevTrialLicense: jest.fn(), @@ -87,6 +89,7 @@ describe('components/feature_discovery', () => { isCloudTrial={false} hadPrevCloudTrial={false} isSubscriptionLoaded={false} + cloudFreeDeprecated={false} isPaidSubscription={false} actions={{ getPrevTrialLicense: jest.fn(), diff --git a/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.tsx b/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.tsx index e2faca2227..902e1a1187 100644 --- a/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.tsx +++ b/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.tsx @@ -59,6 +59,7 @@ type Props = { isSubscriptionLoaded: boolean; isPaidSubscription: boolean; customer?: CloudCustomer; + cloudFreeDeprecated: boolean; } type State = { @@ -205,6 +206,23 @@ export default class FeatureDiscovery extends React.PureComponent extraClass='btn btn-primary' /> ); + if (this.props.cloudFreeDeprecated) { + ctaPrimaryButton = ( + + ); + } } else if (hadPrevCloudTrial) { // if it is cloud, but this account already had a free trial, then the cta button must be Upgrade now ctaPrimaryButton = ( @@ -259,7 +277,7 @@ export default class FeatureDiscovery extends React.PureComponent /> {gettingTrialError} - {(!this.props.isCloud || canRequestCloudFreeTrial) &&

+ {((!this.props.isCloud || canRequestCloudFreeTrial) && !this.props.cloudFreeDeprecated) &&

{canRequestCloudFreeTrial ? ( { + const original = jest.requireActual('mattermost-redux/actions/cloud'); + return { + ...original, + __esModule: true, + + // just testing that it fired, not that the result updated or anything like that + getCloudCustomer: jest.fn(() => ({type: 'bogus'})), + }; +}); + +describe('PaymentAnnouncementBar', () => { + const happyPathStore = { + entities: { + users: { + currentUserId: 'me', + profiles: { + me: { + roles: 'system_admin', + }, + }, + }, + general: { + license: { + Cloud: 'true', + }, + }, + cloud: { + subscription: { + product_id: 'prod_something', + last_invoice: { + status: 'failed', + }, + }, + customer: { + payment_method: { + exp_month: 12, + exp_year: (new Date()).getFullYear() + 1, + }, + }, + products: { + prod_something: { + id: 'prod_something', + sku: CloudProducts.PROFESSIONAL, + }, + }, + }, + }, + views: { + announcementBar: { + announcementBarState: { + announcementBarCount: 1, + }, + }, + }, + }; + + it('when most recent payment failed, shows that', () => { + renderWithIntlAndStore(, happyPathStore); + screen.getByText('Your most recent payment failed'); + }); + + it('when card is expired, shows that', () => { + const store = JSON.parse(JSON.stringify(happyPathStore)); + store.entities.cloud.customer.payment_method.exp_year = (new Date()).getFullYear() - 1; + store.entities.cloud.subscription.last_invoice.status = 'success'; + renderWithIntlAndStore(, store); + screen.getByText('Your credit card has expired', {exact: false}); + }); + + it('when needed, fetches, customer', () => { + const store = JSON.parse(JSON.stringify(happyPathStore)); + store.entities.cloud.customer = null; + store.entities.cloud.subscription.last_invoice.status = 'success'; + renderWithIntlAndStore(, store); + expect(cloudActions.getCloudCustomer).toHaveBeenCalled(); + }); + + it('when not an admin, does not fetch customer', () => { + const store = JSON.parse(JSON.stringify(happyPathStore)); + store.entities.users.profiles.me.roles = ''; + renderWithIntlAndStore(, store); + expect(cloudActions.getCloudCustomer).not.toHaveBeenCalled(); + }); +}); diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.ts b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.ts deleted file mode 100644 index 86e3bd5f05..0000000000 --- a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {connect} from 'react-redux'; -import {bindActionCreators, Dispatch} from 'redux'; - -import {savePreferences} from 'mattermost-redux/actions/preferences'; -import {getLicense} from 'mattermost-redux/selectors/entities/general'; -import {GenericAction} from 'mattermost-redux/types/actions'; -import {getCloudSubscription, getCloudCustomer} from 'mattermost-redux/actions/cloud'; - -import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; -import { - getCloudSubscription as selectCloudSubscription, - getCloudCustomer as selectCloudCustomer, - getSubscriptionProduct, -} from 'mattermost-redux/selectors/entities/cloud'; -import {CloudProducts} from 'utils/constants'; - -import {openModal} from 'actions/views/modals'; - -import {GlobalState} from 'types/store'; - -import PaymentAnnouncementBar from './payment_announcement_bar'; - -function mapStateToProps(state: GlobalState) { - const subscription = selectCloudSubscription(state); - const customer = selectCloudCustomer(state); - const subscriptionProduct = getSubscriptionProduct(state); - return { - userIsAdmin: isCurrentUserSystemAdmin(state), - isCloud: getLicense(state).Cloud === 'true', - subscription, - customer, - isStarterFree: subscriptionProduct?.sku === CloudProducts.STARTER, - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - actions: bindActionCreators( - { - savePreferences, - openModal, - getCloudSubscription, - getCloudCustomer, - }, - dispatch, - ), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(PaymentAnnouncementBar); diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.tsx b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.tsx new file mode 100644 index 0000000000..f14153ad8e --- /dev/null +++ b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.tsx @@ -0,0 +1,89 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useEffect, useState} from 'react'; +import {FormattedMessage} from 'react-intl'; +import {useSelector, useDispatch} from 'react-redux'; +import {isEmpty} from 'lodash'; + +import {DispatchFunc} from 'mattermost-redux/types/actions'; +import {getCloudCustomer} from 'mattermost-redux/actions/cloud'; +import {getLicense} from 'mattermost-redux/selectors/entities/general'; +import { + getCloudSubscription as selectCloudSubscription, + getCloudCustomer as selectCloudCustomer, + getSubscriptionProduct, +} from 'mattermost-redux/selectors/entities/cloud'; +import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; + +import {getHistory} from 'utils/browser_history'; +import {isCustomerCardExpired} from 'utils/cloud_utils'; +import {AnnouncementBarTypes, CloudProducts, ConsolePages} from 'utils/constants'; +import {t} from 'utils/i18n'; + +import AnnouncementBar from '../default_announcement_bar'; + +export default function PaymentAnnouncementBar() { + const [requestedCustomer, setRequestedCustomer] = useState(false); + const dispatch = useDispatch(); + const subscription = useSelector(selectCloudSubscription); + const customer = useSelector(selectCloudCustomer); + const isStarterFree = useSelector(getSubscriptionProduct)?.sku === CloudProducts.STARTER; + const userIsAdmin = useSelector(isCurrentUserSystemAdmin); + const isCloud = useSelector(getLicense).Cloud === 'true'; + + useEffect(() => { + if (isCloud && !isStarterFree && isEmpty(customer) && userIsAdmin && !requestedCustomer) { + setRequestedCustomer(true); + dispatch(getCloudCustomer()); + } + }, + [isCloud, isStarterFree, customer, userIsAdmin, requestedCustomer]); + + const mostRecentPaymentFailed = subscription?.last_invoice?.status === 'failed'; + + if ( + // Prevents banner flashes if the subscription hasn't been loaded yet + isEmpty(subscription) || + isStarterFree || + !isCloud || + !userIsAdmin || + isEmpty(customer) || + (!isCustomerCardExpired(customer) && !mostRecentPaymentFailed) + ) { + return null; + } + + const updatePaymentInfo = () => { + getHistory().push(ConsolePages.PAYMENT_INFO); + }; + + let message = ( + + ); + + if (mostRecentPaymentFailed) { + message = ( + + ); + } + + return ( + + ); +} diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/payment_announcement_bar.tsx b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/payment_announcement_bar.tsx deleted file mode 100644 index 5fe7c7fa4b..0000000000 --- a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/payment_announcement_bar.tsx +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {isEmpty} from 'lodash'; - -import {CloudCustomer, Subscription} from '@mattermost/types/cloud'; - -import {getHistory} from 'utils/browser_history'; -import {isCustomerCardExpired} from 'utils/cloud_utils'; -import {AnnouncementBarTypes} from 'utils/constants'; -import {t} from 'utils/i18n'; - -import AnnouncementBar from '../default_announcement_bar'; - -type Props = { - userIsAdmin: boolean; - isCloud: boolean; - subscription?: Subscription; - customer?: CloudCustomer; - isStarterFree: boolean; - actions: { - getCloudSubscription: () => void; - getCloudCustomer: () => void; - }; -}; - -class PaymentAnnouncementBar extends React.PureComponent { - async componentDidMount() { - if (isEmpty(this.props.customer)) { - await this.props.actions.getCloudCustomer(); - } - } - - isMostRecentPaymentFailed = () => { - return this.props.subscription?.last_invoice?.status === 'failed'; - }; - - shouldShowBanner = () => { - const {userIsAdmin, isCloud, subscription} = this.props; - - // Prevents banner flashes if the subscription hasn't been loaded yet - if (subscription === null) { - return false; - } - - if (this.props.isStarterFree) { - return false; - } - - if (!isCloud) { - return false; - } - - if (!userIsAdmin) { - return false; - } - - if (!isCustomerCardExpired(this.props.customer) && !this.isMostRecentPaymentFailed()) { - return false; - } - - return true; - }; - - updatePaymentInfo = () => { - getHistory().push('/admin_console/billing/payment_info'); - }; - - render() { - if (isEmpty(this.props.customer) || isEmpty(this.props.subscription)) { - return null; - } - - if (!this.shouldShowBanner()) { - return null; - } - - return ( - - - ); - } -} - -export default PaymentAnnouncementBar; diff --git a/webapp/channels/src/components/do_verify_email/do_verify_email.tsx b/webapp/channels/src/components/do_verify_email/do_verify_email.tsx index 0e81ead2c7..52390fa461 100644 --- a/webapp/channels/src/components/do_verify_email/do_verify_email.tsx +++ b/webapp/channels/src/components/do_verify_email/do_verify_email.tsx @@ -6,6 +6,7 @@ import {useIntl} from 'react-intl'; import {useSelector, useDispatch} from 'react-redux'; import {useLocation, useHistory} from 'react-router-dom'; +import {redirectUserToDefaultTeam} from 'actions/global_actions'; import {trackEvent} from 'actions/telemetry_actions.jsx'; import LaptopAlertSVG from 'components/common/svg_images_components/laptop_alert_svg'; @@ -14,6 +15,7 @@ import LoadingScreen from 'components/loading_screen'; import {clearErrors, logError} from 'mattermost-redux/actions/errors'; import {verifyUserEmail, getMe} from 'mattermost-redux/actions/users'; +import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {DispatchFunc} from 'mattermost-redux/types/actions'; @@ -38,6 +40,7 @@ const DoVerifyEmail = () => { const token = params.get('token') ?? ''; const loggedIn = Boolean(useSelector(getCurrentUserId)); + const onboardingFlowEnabled = useSelector(getIsOnboardingFlowEnabled); const [verifyStatus, setVerifyStatus] = useState(VerifyStatus.PENDING); const [serverError, setServerError] = useState(''); @@ -49,11 +52,15 @@ const DoVerifyEmail = () => { const handleRedirect = () => { if (loggedIn) { - // need info about whether admin or not, - // and whether admin has already completed - // first time onboarding. Instead of fetching and orchestrating that here, - // let the default root component handle it. - history.push('/'); + if (onboardingFlowEnabled) { + // need info about whether admin or not, + // and whether admin has already completed + // first time onboarding. Instead of fetching and orchestrating that here, + // let the default root component handle it. + history.push('/'); + return; + } + redirectUserToDefaultTeam(); return; } diff --git a/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap b/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap index 490e16e2da..93cfa8e998 100644 --- a/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap +++ b/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap @@ -9,7 +9,7 @@ Object { aria-controls="CENTER_dropdown_post_id_1" aria-expanded="false" aria-haspopup="true" - aria-label="Actions" + aria-label="more" class="post-menu__item" data-testid="PostDotMenu-Button-post_id_1" id="CENTER_button_post_id_1" @@ -34,7 +34,7 @@ Object { aria-controls="CENTER_dropdown_post_id_1" aria-expanded="false" aria-haspopup="true" - aria-label="Actions" + aria-label="more" class="post-menu__item" data-testid="PostDotMenu-Button-post_id_1" id="CENTER_button_post_id_1" @@ -121,7 +121,7 @@ exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = ` } menuButton={ Object { - "aria-label": "Actions", + "aria-label": "more", "children": , diff --git a/webapp/channels/src/components/dot_menu/dot_menu.tsx b/webapp/channels/src/components/dot_menu/dot_menu.tsx index be3d6afa13..255d05a466 100644 --- a/webapp/channels/src/components/dot_menu/dot_menu.tsx +++ b/webapp/channels/src/components/dot_menu/dot_menu.tsx @@ -497,7 +497,7 @@ export class DotMenuClass extends React.PureComponent { class: classNames('post-menu__item', { 'post-menu__item--active': this.props.isMenuOpen, }), - 'aria-label': formatMessage({id: 'post_info.dot_menu.tooltip.actions', defaultMessage: 'Actions'}), + 'aria-label': formatMessage({id: 'post_info.dot_menu.tooltip.more', defaultMessage: 'More'}).toLowerCase(), children: , }} menu={{ diff --git a/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx b/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx index 547dcd9fc5..c86cc27e0a 100644 --- a/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx +++ b/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx @@ -13,6 +13,7 @@ import {checkHadPriorTrial} from 'mattermost-redux/selectors/entities/cloud'; import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; import {getLicense} from 'mattermost-redux/selectors/entities/general'; import {getPrevTrialLicense} from 'mattermost-redux/actions/admin'; +import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences'; import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn'; import StartTrialBtn from 'components/learn_more_trial_modal/start_trial_btn'; @@ -59,6 +60,7 @@ const FeatureRestrictedModal = ({ dispatch(getPrevTrialLicense()); }, []); + const cloudFreeDeprecated = useSelector(deprecateCloudFree); const hasCloudPriorTrial = useSelector(checkHadPriorTrial); const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense); const hasSelfHostedPriorTrial = prevTrialLicense.IsLicensed === 'true'; @@ -100,7 +102,7 @@ const FeatureRestrictedModal = ({ const getTitle = () => { if (isSystemAdmin) { - return hasPriorTrial ? titleAdminPostTrial : titleAdminPreTrial; + return (hasPriorTrial || cloudFreeDeprecated) ? titleAdminPostTrial : titleAdminPreTrial; } return titleEndUser; @@ -108,13 +110,13 @@ const FeatureRestrictedModal = ({ const getMessage = () => { if (isSystemAdmin) { - return hasPriorTrial ? messageAdminPostTrial : messageAdminPreTrial; + return (hasPriorTrial || cloudFreeDeprecated) ? messageAdminPostTrial : messageAdminPreTrial; } return messageEndUser; }; - const showStartTrial = isSystemAdmin && !hasPriorTrial; + const showStartTrial = isSystemAdmin && !hasPriorTrial && !cloudFreeDeprecated; // define what is the secondary button text and action, by default will be the View Plan button let secondaryBtnMsg = formatMessage({id: 'feature_restricted_modal.button.plans', defaultMessage: 'View plans'}); diff --git a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts index 5a2ac01c35..80bdf8f328 100644 --- a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts +++ b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts @@ -8,6 +8,7 @@ import {withRouter} from 'react-router-dom'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {GenericAction} from 'mattermost-redux/types/actions'; import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams'; +import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users'; import {getUserGuideDropdownPluginMenuItems} from 'selectors/plugins'; @@ -31,6 +32,7 @@ function mapStateToProps(state: GlobalState) { teamUrl: getCurrentRelativeTeamUrl(state), pluginMenuItems: getUserGuideDropdownPluginMenuItems(state), isFirstAdmin: isFirstAdmin(state), + onboardingFlowEnabled: getIsOnboardingFlowEnabled(state), }; } diff --git a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx index aa1ac2e833..ed4e5532b9 100644 --- a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx +++ b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx @@ -34,6 +34,7 @@ describe('components/channel_header/components/UserGuideDropdown', () => { }, pluginMenuItems: [], isFirstAdmin: false, + onboardingFlowEnabled: false, }; test('should match snapshot', () => { diff --git a/webapp/channels/src/components/invitation_modal/invite_as.tsx b/webapp/channels/src/components/invitation_modal/invite_as.tsx index aabcb2674e..a7e552c024 100644 --- a/webapp/channels/src/components/invitation_modal/invite_as.tsx +++ b/webapp/channels/src/components/invitation_modal/invite_as.tsx @@ -14,6 +14,7 @@ import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/user import {getSubscriptionProduct, checkHadPriorTrial} from 'mattermost-redux/selectors/entities/cloud'; import {DispatchFunc} from 'mattermost-redux/types/actions'; import {getPrevTrialLicense} from 'mattermost-redux/actions/admin'; +import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences'; import {closeModal, openModal} from 'actions/views/modals'; @@ -43,6 +44,7 @@ export type Props = { export default function InviteAs(props: Props) { const {formatMessage} = useIntl(); const license = useSelector(getLicense); + const cloudFreeDeprecated = useSelector(deprecateCloudFree); const dispatch = useDispatch(); useEffect(() => { @@ -85,7 +87,7 @@ export default function InviteAs(props: Props) { if (isFreeTrial) { ctaExtraContentMsg = formatMessage({id: 'free.professional_feature.professional', defaultMessage: 'Professional feature'}); } else { - ctaExtraContentMsg = hasPriorTrial ? formatMessage({id: 'free.professional_feature.upgrade', defaultMessage: 'Upgrade'}) : formatMessage({id: 'free.professional_feature.try_free', defaultMessage: 'Professional feature- try it out free'}); + ctaExtraContentMsg = (hasPriorTrial || cloudFreeDeprecated) ? formatMessage({id: 'free.professional_feature.upgrade', defaultMessage: 'Upgrade'}) : formatMessage({id: 'free.professional_feature.try_free', defaultMessage: 'Professional feature- try it out free'}); } const restrictedIndicator = ( diff --git a/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.test.tsx b/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.test.tsx index e9dd3a2147..2194f15855 100644 --- a/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.test.tsx +++ b/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.test.tsx @@ -33,6 +33,12 @@ describe('components/learn_more_trial_modal/learn_more_trial_modal', () => { entities: { users: { currentUserId: 'current_user_id', + profiles: { + current_user_id: { + id: 'current_user_id', + roles: '', + }, + }, }, admin: { analytics: { diff --git a/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.tsx b/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.tsx index d3a81f9690..c50aa6caa8 100644 --- a/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.tsx +++ b/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {useIntl} from 'react-intl'; +import {FormattedMessage, useIntl} from 'react-intl'; import {useSelector, useDispatch} from 'react-redux'; import {trackEvent} from 'actions/telemetry_actions'; @@ -16,10 +16,13 @@ import MonitorImacLikeSVG from 'components/common/svg_images_components/monitor_ import SystemRolesSVG from 'components/admin_console/feature_discovery/features/images/system_roles_svg'; import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn'; import {BtnStyle} from 'components/common/carousel/carousel_button'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; +import ExternalLink from 'components/external_link'; import {closeModal} from 'actions/views/modals'; import {DispatchFunc} from 'mattermost-redux/types/actions'; import {getLicense} from 'mattermost-redux/selectors/entities/general'; +import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences'; import StartTrialBtn from './start_trial_btn'; @@ -43,8 +46,11 @@ const LearnMoreTrialModal = ( const [embargoed, setEmbargoed] = useState(false); const dispatch = useDispatch(); + const [, salesLink] = useOpenSalesLink(); + // Cloud conditions const license = useSelector(getLicense); + const cloudFreeDeprecated = useSelector(deprecateCloudFree); const isCloud = license?.Cloud === 'true'; const handleEmbargoError = useCallback(() => { @@ -78,6 +84,20 @@ const LearnMoreTrialModal = ( extraClass={'btn btn-primary start-cloud-trial-btn'} /> ); + if (cloudFreeDeprecated) { + startTrialBtn = ( + + + + ); + } } const handleOnClose = useCallback(() => { diff --git a/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal_step.tsx b/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal_step.tsx index b9b3bbba43..3b721fea0b 100644 --- a/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal_step.tsx +++ b/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal_step.tsx @@ -2,10 +2,11 @@ // See LICENSE.txt for license information. import React from 'react'; - +import {useSelector} from 'react-redux'; import {FormattedMessage} from 'react-intl'; import TrialBenefitsModalStepMore from 'components/trial_benefits_modal/trial_benefits_modal_step_more'; +import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences'; import './learn_more_trial_modal_step.scss'; import {AboutLinks, LicenseLinks} from 'utils/constants'; @@ -35,6 +36,7 @@ const LearnMoreTrialModalStep = ( buttonLabel, handleOnClose, }: LearnMoreTrialModalStepProps) => { + const cloudFreeDeprecated = useSelector(deprecateCloudFree); return (

)} -
- - ( - - {msg} - - ), - linkPrivacy: (msg: React.ReactNode) => ( - - {msg} - - ), - }} - /> - -
+ { + cloudFreeDeprecated ? '' : ( +
+ + ( + + {msg} + + ), + linkPrivacy: (msg: React.ReactNode) => ( + + {msg} + + ), + }} + /> + +
+ ) + } {bottomLeftMessage && (
{bottomLeftMessage} diff --git a/webapp/channels/src/components/login/login.tsx b/webapp/channels/src/components/login/login.tsx index edde751e63..f72e7df0f0 100644 --- a/webapp/channels/src/components/login/login.tsx +++ b/webapp/channels/src/components/login/login.tsx @@ -13,7 +13,7 @@ import {UserProfile} from '@mattermost/types/users'; import {Client4} from 'mattermost-redux/client'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {getIsOnboardingFlowEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getTeamByName, getMyTeamMember} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {isSystemAdmin} from 'mattermost-redux/utils/user_utils'; @@ -104,6 +104,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => { const currentUser = useSelector(getCurrentUser); const experimentalPrimaryTeam = useSelector((state: GlobalState) => (ExperimentalPrimaryTeam ? getTeamByName(state, ExperimentalPrimaryTeam) : undefined)); const experimentalPrimaryTeamMember = useSelector((state: GlobalState) => getMyTeamMember(state, experimentalPrimaryTeam?.id ?? '')); + const onboardingFlowEnabled = useSelector(getIsOnboardingFlowEnabled); const isCloud = useSelector(isCurrentLicenseCloud); const graphQLEnabled = useSelector(isGraphQLEnabled); @@ -634,12 +635,14 @@ const Login = ({onCustomizeHeader}: LoginProps) => { } else if (experimentalPrimaryTeamMember.team_id) { // Only set experimental team if user is on that team history.push(`/${ExperimentalPrimaryTeam}`); - } else { + } else if (onboardingFlowEnabled) { // need info about whether admin or not, // and whether admin has already completed // first time onboarding. Instead of fetching and orchestrating that here, // let the default root component handle it. history.push('/'); + } else { + redirectUserToDefaultTeam(); } }; diff --git a/webapp/channels/src/components/post/index.tsx b/webapp/channels/src/components/post/index.tsx index 39c664736f..8ac03cbb63 100644 --- a/webapp/channels/src/components/post/index.tsx +++ b/webapp/channels/src/components/post/index.tsx @@ -13,7 +13,7 @@ import { getBool, isCollapsedThreadsEnabled, } from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeam, getCurrentTeamId, getTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentTeam, getTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; import {Emoji} from '@mattermost/types/emojis'; @@ -48,7 +48,6 @@ interface OwnProps { post?: Post | UserActivityPost; previousPostId?: string; postId?: string; - teamId?: string; shouldHighlight?: boolean; location: keyof typeof Locations; } @@ -120,7 +119,6 @@ function makeMapStateToProps() { const config = getConfig(state); const enableEmojiPicker = config.EnableEmojiPicker === 'true'; const enablePostUsernameOverride = config.EnablePostUsernameOverride === 'true'; - const teamId = ownProps.teamId || getCurrentTeamId(state); const channel = state.entities.channels.channels[post.channel_id]; const shortcutReactToLastPostEmittedFrom = getShortcutReactToLastPostEmittedFrom(state); @@ -148,6 +146,7 @@ function makeMapStateToProps() { } const currentTeam = getCurrentTeam(state); + const team = getTeam(state, channel.team_id); let teamName = currentTeam.name; let teamDisplayName = ''; @@ -159,7 +158,6 @@ function makeMapStateToProps() { !isDMorGM && // Not show for DM or GMs since they don't belong to a team memberships && Object.values(memberships).length > 1 // Not show if the user only belongs to one team ) { - const team = getTeam(state, channel.team_id); teamDisplayName = team?.display_name; teamName = team?.name || currentTeam.name; } @@ -186,7 +184,6 @@ function makeMapStateToProps() { enablePostUsernameOverride, isEmbedVisible: isEmbedVisible(state, post.id), isReadOnly: false, - teamId, currentUserId: getCurrentUserId(state), isFirstReply: previousPost ? isFirstReply(post, previousPost) : false, hasReplies: getReplyCount(state, post) > 0, @@ -200,7 +197,8 @@ function makeMapStateToProps() { compactDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, colorizeUsernames: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLORIZE_USERNAMES, Preferences.COLORIZE_USERNAMES_DEFAULT) === 'true', shouldShowActionsMenu: shouldShowActionsMenu(state, post), - + currentTeam, + team, shortcutReactToLastPostEmittedFrom, isBot, collapsedThreadsEnabled: isCollapsedThreadsEnabled(state), diff --git a/webapp/channels/src/components/post/post_component.tsx b/webapp/channels/src/components/post/post_component.tsx index 8a549c272e..52ee868f3d 100644 --- a/webapp/channels/src/components/post/post_component.tsx +++ b/webapp/channels/src/components/post/post_component.tsx @@ -50,10 +50,12 @@ import {Emoji} from '@mattermost/types/emojis'; import PostUserProfile from './user_profile'; import PostOptions from './post_options'; +import {Team} from '@mattermost/types/teams'; export type Props = { post: Post; - teamId: string; + currentTeam: Team; + team?: Team; currentUserId: string; compactDisplay?: boolean; colorizeUsernames?: boolean; @@ -123,6 +125,7 @@ const PostComponent = (props: Props): JSX.Element => { const isRHS = props.location === Locations.RHS_ROOT || props.location === Locations.RHS_COMMENT || props.location === Locations.SEARCH; const postRef = useRef(null); const postHeaderRef = useRef(null); + const teamId = props.team?.id || props.currentTeam.id; const [hover, setHover] = useState(false); const [a11yActive, setA11y] = useState(false); @@ -355,7 +358,15 @@ const PostComponent = (props: Props): JSX.Element => { return; } props.actions.selectPostFromRightHandSideSearch(post); - }, [post, props.actions]); + }, [post, props.actions, props.actions.selectPostFromRightHandSideSearch]); + + const handleThreadClick = useCallback((e: React.MouseEvent) => { + if (props.currentTeam.id === props.team?.id) { + handleCommentClick(e); + } else { + handleJumpClick(e); + } + }, [handleCommentClick, handleJumpClick]); const postClass = classNames('post__body', {'post--edited': PostUtils.isEdited(post), 'search-item-snippet': isSearchResultItem}); @@ -435,7 +446,7 @@ const PostComponent = (props: Props): JSX.Element => { const threadFooter = props.location !== Locations.RHS_ROOT && props.isCollapsedThreadsEnabled && !post.root_id && (props.hasReplies || post.is_following) ? ( ) : null; const currentPostDay = getDateForUnixTicks(post.create_at); @@ -538,6 +549,7 @@ const PostComponent = (props: Props): JSX.Element => { {((!hideProfilePicture && props.location === Locations.CENTER) || hover || props.location !== Locations.CENTER) && { {!props.isPostBeingEdited && { /> ); - botIndicator = (); + // user profile component checks and add bot tag in case webhook is from bot account, but if webhook is from user account we need this. + + if (!isBot) { + botIndicator = (); + } } else if (isFromAutoResponder) { userProfile = ( diff --git a/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap index 4aa2442f0a..eabb389cb1 100644 --- a/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap +++ b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap @@ -71,7 +71,145 @@ exports[`InviteMembers component should match snapshot 1`] = `
+
+
+
+
+`; + +exports[`InviteMembers component should match snapshot when it is cloud 1`] = ` +
+
+
+
+
+
+ Previous step +
+

+ + Who works with you? + +

+

+ + Collaboration is tough by yourself. Invite a few team members. Separate each email address with a space or comma. + +

+
+
+ +

+   + +

+

+   + 0 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu. +

+
+
+
+
+ Enter email addresses +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
diff --git a/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap index 0610646391..1abd12f11b 100644 --- a/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap +++ b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap @@ -1,6 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/preparing-workspace/invite_members_link should match snapshot 1`] = ` +
+ +
+`; + +exports[`components/preparing-workspace/invite_members_link should match snapshot when displayed including the input field 1`] = `