1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-24 13:43:12 +02:00

Merge branch 'main' into weblate-focalboard-webapp

This commit is contained in:
Mattermost Build 2023-03-30 17:17:12 +03:00 committed by GitHub
commit 4af46a43b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 205 additions and 559 deletions

View File

@ -15,7 +15,7 @@ env:
jobs:
ci-ubuntu-server:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
strategy:
matrix:
@ -44,7 +44,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Set up Go
uses: actions/setup-go@v3
with:
@ -54,7 +54,7 @@ jobs:
run: cd focalboard; make server-test-${{matrix['db']}}
ci-ubuntu-webapp:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
@ -74,7 +74,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: npm ci
run: |
cd focalboard/webapp && npm ci && cd -
@ -132,7 +132,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Set up Go
uses: actions/setup-go@v3
@ -169,7 +169,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Set up Go
uses: actions/setup-go@v3

View File

@ -8,14 +8,13 @@ on:
workflow_dispatch:
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
EXCLUDE_ENTERPRISE: true
jobs:
ubuntu:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
@ -25,16 +24,16 @@ jobs:
continue-on-error: true
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref: ${{ env.BRANCH_NAME }}
- uses: actions/checkout@v3
if: steps.mattermostServer.outcome == 'failure'
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -101,16 +100,16 @@ jobs:
continue-on-error: true
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref: ${{ env.BRANCH_NAME }}
- uses: actions/checkout@v3
if: steps.mattermostServer.outcome == 'failure'
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -159,16 +158,16 @@ jobs:
continue-on-error: true
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref: ${{ env.BRANCH_NAME }}
- uses: actions/checkout@v3
if: steps.mattermostServer.outcome == 'failure'
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -218,7 +217,7 @@ jobs:
path: ${{ github.workspace }}/focalboard/win-wpf/dist/focalboard-win.zip
plugin:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@ -229,16 +228,16 @@ jobs:
continue-on-error: true
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref: ${{ env.BRANCH_NAME }}
- uses: actions/checkout@v3
if: steps.mattermostServer.outcome == 'failure'
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go

View File

@ -13,7 +13,7 @@ env:
jobs:
down-migrations:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
@ -26,7 +26,7 @@ jobs:
golangci:
name: plugin
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
@ -48,7 +48,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: set up golangci-lint
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.1
- name: lint

View File

@ -9,7 +9,7 @@ env:
jobs:
ubuntu:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -21,16 +21,16 @@ jobs:
continue-on-error: true
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref: ${{ env.BRANCH_NAME }}
- uses: actions/checkout@v3
if: steps.mattermostServer.outcome == 'failure'
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -97,16 +97,16 @@ jobs:
continue-on-error: true
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref: ${{ env.BRANCH_NAME }}
- uses: actions/checkout@v3
if: steps.mattermostServer.outcome == 'failure'
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -156,16 +156,16 @@ jobs:
continue-on-error: true
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref: ${{ env.BRANCH_NAME }}
- uses: actions/checkout@v3
if: steps.mattermostServer.outcome == 'failure'
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -216,7 +216,7 @@ jobs:
path: ${{ github.workspace }}/focalboard/win-wpf/dist/focalboard-win.zip
plugin-release:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -228,16 +228,16 @@ jobs:
continue-on-error: true
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref: ${{ env.BRANCH_NAME }}
- uses: actions/checkout@v3
if: steps.mattermostServer.outcome == 'failure'
with:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go

View File

@ -868,10 +868,8 @@ func (s *SQLStore) doesDuplicateCategoryBoardsExist() (bool, error) {
}
func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error {
query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " +
"FROM " + s.tablePrefix + "category_boards) " +
"DELETE " + s.tablePrefix + "category_boards FROM " + s.tablePrefix + "category_boards " +
"JOIN duplicates USING(id) WHERE duplicates.rownum > 1;"
query := "DELETE FROM " + s.tablePrefix + "category_boards WHERE id NOT IN " +
"(SELECT * FROM ( SELECT min(id) FROM " + s.tablePrefix + "category_boards GROUP BY user_id, board_id ) as data)"
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err))
}

View File

@ -319,7 +319,7 @@ func (s *SQLStore) GetTemplateHelperFuncs() template.FuncMap {
func (s *SQLStore) genAddColumnIfNeeded(tableName, columnName, datatype, constraint string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
normTableName := s.normalizeTablename(tableName)
switch s.dbType {
case model.SqliteDBType:
@ -358,7 +358,7 @@ func (s *SQLStore) genAddColumnIfNeeded(tableName, columnName, datatype, constra
func (s *SQLStore) genDropColumnIfNeeded(tableName, columnName string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
normTableName := s.normalizeTablename(tableName)
switch s.dbType {
case model.SqliteDBType:
@ -395,7 +395,7 @@ func (s *SQLStore) genDropColumnIfNeeded(tableName, columnName string) (string,
func (s *SQLStore) genCreateIndexIfNeeded(tableName, columns string) (string, error) {
indexName := getIndexName(tableName, columns)
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
normTableName := s.normalizeTablename(tableName)
switch s.dbType {
case model.SqliteDBType:
@ -435,7 +435,7 @@ func (s *SQLStore) genRenameTableIfNeeded(oldTableName, newTableName string) (st
oldTableName = addPrefixIfNeeded(oldTableName, s.tablePrefix)
newTableName = addPrefixIfNeeded(newTableName, s.tablePrefix)
normOldTableName := normalizeTablename(s.schemaName, oldTableName)
normOldTableName := s.normalizeTablename(oldTableName)
vars := map[string]string{
"schema": s.schemaName,
@ -466,14 +466,14 @@ func (s *SQLStore) genRenameTableIfNeeded(oldTableName, newTableName string) (st
case model.PostgresDBType:
return replaceVars(`
do $$
begin
begin
if (SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = '[[new_table_name]]'
AND table_schema = '[[schema]]'
) = 0 then
) = 0 then
ALTER TABLE [[norm_old_table_name]] RENAME TO [[new_table_name]];
end if;
end$$;
end$$;
`, vars), nil
default:
return "", ErrUnsupportedDatabaseType
@ -482,7 +482,7 @@ func (s *SQLStore) genRenameTableIfNeeded(oldTableName, newTableName string) (st
func (s *SQLStore) genRenameColumnIfNeeded(tableName, oldColumnName, newColumnName, dataType string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
normTableName := s.normalizeTablename(tableName)
vars := map[string]string{
"schema": s.schemaName,
@ -516,15 +516,15 @@ func (s *SQLStore) genRenameColumnIfNeeded(tableName, oldColumnName, newColumnNa
case model.PostgresDBType:
return replaceVars(`
do $$
begin
begin
if (SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND column_name = '[[new_column_name]]'
) = 0 then
) = 0 then
ALTER TABLE [[norm_table_name]] RENAME COLUMN [[old_column_name]] TO [[new_column_name]];
end if;
end$$;
end$$;
`, vars), nil
default:
return "", ErrUnsupportedDatabaseType
@ -620,7 +620,7 @@ func (s *SQLStore) doesColumnExist(tableName, columnName string) (bool, error) {
func (s *SQLStore) genAddConstraintIfNeeded(tableName, constraintName, constraintType, constraintDefinition string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
normTableName := s.normalizeTablename(tableName)
var query string
@ -686,8 +686,12 @@ func addPrefixIfNeeded(s, prefix string) string {
return s
}
func normalizeTablename(schemaName, tableName string) string {
if schemaName != "" && !strings.HasPrefix(tableName, schemaName+".") {
func (s *SQLStore) normalizeTablename(tableName string) string {
if s.schemaName != "" && !strings.HasPrefix(tableName, s.schemaName+".") {
schemaName := s.schemaName
if s.dbType == model.MysqlDBType {
schemaName = "`" + schemaName + "`"
}
tableName = schemaName + "." + tableName
}
return tableName

View File

@ -246,6 +246,9 @@ func (bm *BoardsMigrator) MigrateToStep(step int) error {
func (bm *BoardsMigrator) Interceptors() map[int]foundation.Interceptor {
return map[int]foundation.Interceptor{
18: bm.store.RunDeletedMembershipBoardsMigration,
35: func() error {
return bm.store.RunDeDuplicateCategoryBoardsMigration(35)
},
}
}

View File

@ -0,0 +1,28 @@
package migrationstests
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestRunDeDuplicateCategoryBoardsMigration(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
if th.IsSQLite() {
t.Skip("SQLite is not supported for this")
}
th.f.MigrateToStepSkippingLastInterceptor(35).
ExecFile("./fixtures/testDeDuplicateCategoryBoardsMigration.sql")
th.f.RunInterceptor(35)
// verifying count of rows
var count int
countQuery := "SELECT COUNT(*) FROM focalboard_category_boards"
row := th.f.DB().QueryRow(countQuery)
err := row.Scan(&count)
assert.NoError(t, err)
assert.Equal(t, 4, count)
}

View File

@ -1,6 +1,8 @@
INSERT INTO focalboard_category_boards values
INSERT INTO focalboard_category_boards
(id, user_id, category_id, board_id, create_at, update_at, delete_at, sort_order)
values
('id-1', 'user_id-1', 'category-id-1', 'board-id-1', 1672988834402, 1672988834402, 0, 0),
('id-2', 'user_id-1', 'category-id-2', 'board-id-1', 1672988834402, 1672988834402, 0, 0),
('id-3', 'user_id-2', 'category-id-3', 'board-id-2', 1672988834402, 1672988834402, 1672988834402, 0),
('id-4', 'user_id-2', 'category-id-3', 'board-id-4', 1672988834402, 1672988834402, 0, 0),
('id-5', 'user_id-3', 'category-id-4', 'board-id-3', 1672988834402, 1672988834402, 1672988834402, 0);
('id-5', 'user_id-3', 'category-id-4', 'board-id-3', 1672988834402, 1672988834402, 1672988834402, 0);

View File

@ -1,6 +1,8 @@
INSERT INTO focalboard_category_boards values
INSERT INTO focalboard_category_boards
(id, user_id, category_id, board_id, create_at, update_at, delete_at, sort_order)
values
('id-1', 'user_id-1', 'category-id-1', 'board-id-1', 1672988834402, 1672988834402, 0, 0),
('id-2', 'user_id-1', 'category-id-2', 'board-id-1', 1672988834402, 1672988834402, 0, 0),
('id-3', 'user_id-2', 'category-id-3', 'board-id-2', 1672988834402, 1672988834402, 0, 0),
('id-4', 'user_id-2', 'category-id-3', 'board-id-4', 1672988834402, 1672988834402, 0, 0),
('id-5', 'user_id-3', 'category-id-4', 'board-id-3', 1672988834402, 1672988834402, 0, 0);
('id-5', 'user_id-3', 'category-id-4', 'board-id-3', 1672988834402, 1672988834402, 0, 0);

View File

@ -1,4 +1,6 @@
INSERT INTO focalboard_category_boards VALUES
INSERT INTO focalboard_category_boards
(id, user_id, category_id, board_id, create_at, update_at, sort_order, hidden)
VALUES
('id-1', 'user-id-1', 'category-id-1', 'board-id-1', 1672889246832, 1672889246832, 0, false),
('id-2', 'user-id-1', 'category-id-2', 'board-id-2', 1672889246832, 1672889246832, 0, false),
('id-3', 'user-id-2', 'category-id-3', 'board-id-3', 1672889246832, 1672889246832, 0, false),
@ -7,4 +9,4 @@ INSERT INTO focalboard_category_boards VALUES
INSERT INTO Preferences VALUES
('user-id-1', 'focalboard', 'hiddenBoardIDs', '["board-id-1"]'),
('user-id-2', 'focalboard', 'hiddenBoardIDs', '["board-id-3", "board-id-4"]');
('user-id-2', 'focalboard', 'hiddenBoardIDs', '["board-id-3", "board-id-4"]');

View File

@ -1,6 +1,8 @@
INSERT INTO focalboard_category_boards VALUES
INSERT INTO focalboard_category_boards
(id, user_id, category_id, board_id, create_at, update_at, sort_order, hidden)
VALUES
('id-1', 'user-id-1', 'category-id-1', 'board-id-1', 1672889246832, 1672889246832, 0, false),
('id-2', 'user-id-1', 'category-id-2', 'board-id-2', 1672889246832, 1672889246832, 0, false),
('id-3', 'user-id-2', 'category-id-3', 'board-id-3', 1672889246832, 1672889246832, 0, false),
('id-4', 'user-id-2', 'category-id-3', 'board-id-4', 1672889246832, 1672889246832, 0, false),
('id-5', 'user-id-3', 'category-id-4', 'board-id-5', 1672889246832, 1672889246832, 0, false);
('id-5', 'user-id-3', 'category-id-4', 'board-id-5', 1672889246832, 1672889246832, 0, false);

View File

@ -1,4 +1,6 @@
INSERT INTO focalboard_category_boards VALUES
INSERT INTO focalboard_category_boards
(id, user_id, category_id, board_id, create_at, update_at, sort_order, hidden)
VALUES
('id-1', 'user-id-1', 'category-id-1', 'board-id-1', 1672889246832, 1672889246832, 0, false),
('id-2', 'user-id-1', 'category-id-2', 'board-id-2', 1672889246832, 1672889246832, 0, false),
('id-3', 'user-id-2', 'category-id-3', 'board-id-3', 1672889246832, 1672889246832, 0, false),
@ -7,4 +9,4 @@ INSERT INTO focalboard_category_boards VALUES
INSERT INTO Preferences VALUES
('user-id-1', 'focalboard', 'hiddenBoardIDs', ''),
('user-id-2', 'focalboard', 'hiddenBoardIDs', '');
('user-id-2', 'focalboard', 'hiddenBoardIDs', '');

View File

@ -1,4 +1,6 @@
INSERT INTO focalboard_category_boards VALUES
INSERT INTO focalboard_category_boards
(id, user_id, category_id, board_id, create_at, update_at, sort_order, hidden)
VALUES
('id-1', 'user-id-1', 'category-id-1', 'board-id-1', 1672889246832, 1672889246832, 0, false),
('id-2', 'user-id-1', 'category-id-2', 'board-id-2', 1672889246832, 1672889246832, 0, false),
('id-3', 'user-id-2', 'category-id-3', 'board-id-3', 1672889246832, 1672889246832, 0, false),
@ -7,4 +9,4 @@ INSERT INTO focalboard_category_boards VALUES
INSERT INTO focalboard_preferences VALUES
('user-id-1', 'focalboard', 'hiddenBoardIDs', '["board-id-1"]'),
('user-id-2', 'focalboard', 'hiddenBoardIDs', '["board-id-3", "board-id-4"]');
('user-id-2', 'focalboard', 'hiddenBoardIDs', '["board-id-3", "board-id-4"]');

View File

@ -1,4 +1,6 @@
INSERT INTO focalboard_category_boards VALUES
INSERT INTO focalboard_category_boards
(id, user_id, category_id, board_id, create_at, update_at, sort_order, hidden)
VALUES
('id-1', 'user-id-1', 'category-id-1', 'board-id-1', 1672889246832, 1672889246832, 0, false),
('id-2', 'user-id-1', 'category-id-2', 'board-id-2', 1672889246832, 1672889246832, 0, false),
('id-3', 'user-id-2', 'category-id-3', 'board-id-3', 1672889246832, 1672889246832, 0, false),
@ -7,4 +9,4 @@ INSERT INTO focalboard_category_boards VALUES
INSERT INTO focalboard_preferences VALUES
('user-id-1', 'focalboard', 'hiddenBoardIDs', ''),
('user-id-2', 'focalboard', 'hiddenBoardIDs', '');
('user-id-2', 'focalboard', 'hiddenBoardIDs', '');

View File

@ -0,0 +1,9 @@
INSERT INTO focalboard_category_boards(id, user_id, category_id, board_id, create_at, update_at, sort_order)
VALUES
('id_1', 'user_id_1', 'category_id_1', 'board_id_1', 0, 0, 0),
('id_2', 'user_id_1', 'category_id_2', 'board_id_1', 0, 0, 0),
('id_3', 'user_id_1', 'category_id_3', 'board_id_1', 0, 0, 0),
('id_4', 'user_id_2', 'category_id_4', 'board_id_2', 0, 0, 0),
('id_5', 'user_id_2', 'category_id_5', 'board_id_2', 0, 0, 0),
('id_6', 'user_id_3', 'category_id_6', 'board_id_3', 0, 0, 0),
('id_7', 'user_id_4', 'category_id_6', 'board_id_4', 0, 0, 0);

View File

@ -465,10 +465,15 @@ func (ws *Server) getListenersForBlock(blockID string) []*websocketSession {
return ws.listenersByBlock[blockID]
}
// getListenersForTeam returns the listeners subscribed to a
// getListenersForUser returns the listener for a user subscribed to a
// team changes.
func (ws *Server) getListenersForTeam(teamID string) []*websocketSession {
return ws.listenersByTeam[teamID]
func (ws *Server) getListenerForUser(teamID, userID string) *websocketSession {
for _, listener := range ws.listenersByTeam[teamID] {
if listener.userID == userID {
return listener
}
}
return nil
}
// getListenersForTeamAndBoard returns the listeners subscribed to a
@ -567,16 +572,10 @@ func (ws *Server) BroadcastCategoryChange(category model.Category) {
Category: &category,
}
listeners := ws.getListenersForTeam(category.TeamID)
ws.logger.Debug("listener(s) for teamID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", category.TeamID),
mlog.String("categoryID", category.ID),
)
for _, listener := range listeners {
ws.logger.Debug("Broadcast block change",
mlog.Int("listener_count", len(listeners)),
listener := ws.getListenerForUser(category.TeamID, category.UserID)
if listener != nil {
ws.logger.Debug("Broadcast category change",
mlog.String("userID", category.UserID),
mlog.String("teamID", category.TeamID),
mlog.String("categoryID", category.ID),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
@ -596,15 +595,10 @@ func (ws *Server) BroadcastCategoryReorder(teamID, userID string, categoryOrder
TeamID: teamID,
}
listeners := ws.getListenersForTeam(teamID)
ws.logger.Debug("listener(s) for teamID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
)
for _, listener := range listeners {
listener := ws.getListenerForUser(teamID, userID)
if listener != nil {
ws.logger.Debug("Broadcast category order change",
mlog.Int("listener_count", len(listeners)),
mlog.String("userID", userID),
mlog.String("teamID", teamID),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
@ -624,21 +618,17 @@ func (ws *Server) BroadcastCategoryBoardsReorder(teamID, userID, categoryID stri
TeamID: teamID,
}
listeners := ws.getListenersForTeam(teamID)
ws.logger.Debug("listener(s) for teamID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
)
for _, listener := range listeners {
listener := ws.getListenerForUser(teamID, userID)
if listener != nil {
ws.logger.Debug("Broadcast board category order change",
mlog.Int("listener_count", len(listeners)),
mlog.String("userID", userID),
mlog.String("teamID", teamID),
mlog.String("categoryID", categoryID),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
if err := listener.WriteJSON(message); err != nil {
ws.logger.Error("broadcast category order change error", mlog.Err(err))
ws.logger.Error("broadcast category boards order change error", mlog.Err(err))
listener.conn.Close()
}
}
@ -651,16 +641,10 @@ func (ws *Server) BroadcastCategoryBoardChange(teamID, userID string, boardCateg
BoardCategories: boardCategories,
}
listeners := ws.getListenersForTeam(teamID)
ws.logger.Debug("listener(s) for teamID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
mlog.Int("numEntries", len(boardCategories)),
)
for _, listener := range listeners {
ws.logger.Debug("Broadcast block change",
mlog.Int("listener_count", len(listeners)),
listener := ws.getListenerForUser(teamID, userID)
if listener != nil {
ws.logger.Debug("Broadcast category board change",
mlog.String("userID", userID),
mlog.String("teamID", teamID),
mlog.Int("numEntries", len(boardCategories)),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),

View File

@ -101,6 +101,48 @@ func TestTeamSubscription(t *testing.T) {
require.Empty(t, server.listenersByTeam[teamID])
require.Empty(t, server.listenersByTeam[teamID2])
})
t.Run("Subscribe users to team retrieve by user", func(t *testing.T) {
userID1 := "fake-user-id"
userSession1 := &websocketSession{
conn: &websocket.Conn{},
mu: sync.Mutex{},
userID: userID1,
teams: []string{},
blocks: []string{},
}
userID2 := "fake-user-id2"
userSession2 := &websocketSession{
conn: &websocket.Conn{},
mu: sync.Mutex{},
userID: userID2,
teams: []string{},
blocks: []string{},
}
teamID := "fake-team-id"
server.addListener(session)
server.subscribeListenerToTeam(session, teamID)
server.addListener(userSession1)
server.subscribeListenerToTeam(userSession1, teamID)
server.addListener(userSession2)
server.subscribeListenerToTeam(userSession2, teamID)
require.Len(t, server.listeners, 3)
require.Len(t, server.listenersByTeam[teamID], 3)
listener := server.getListenerForUser(teamID, userID1)
require.NotNil(t, listener)
require.Equal(t, listener.userID, userID1)
server.removeListener(session)
server.removeListener(userSession1)
server.removeListener(userSession2)
require.Empty(t, server.listeners)
require.Empty(t, server.listenersByTeam[teamID])
require.Empty(t, server.getListenerForUser(teamID, userID1))
})
}
func TestBlocksSubscription(t *testing.T) {

View File

@ -1,5 +1,7 @@
{
"AppBar.Tooltip": "Toggle linked boards",
"AdminBadge.SystemAdmin": "Admin",
"AdminBadge.TeamAdmin": "Team Admin",
"AppBar.Tooltip": "Toggle Linked Boards",
"Attachment.Attachment-title": "Attachment",
"AttachmentBlock.DeleteAction": "delete",
"AttachmentBlock.addElement": "add {type}",
@ -115,7 +117,6 @@
"CenterPanel.Login": "Login",
"CenterPanel.Share": "Share",
"ChannelIntro.CreateBoard": "Create a board",
"CloudMessage.cloud-server": "Get your own free cloud server.",
"ColorOption.selectColor": "Select {color} Color",
"Comment.delete": "Delete",
"CommentsList.send": "Send",
@ -138,6 +139,7 @@
"ContentBlock.moveDown": "Move down",
"ContentBlock.moveUp": "Move up",
"ContentBlock.text": "text",
"DateFilter.empty": "Empty",
"DateRange.clear": "Clear",
"DateRange.empty": "Empty",
"DateRange.endDate": "End date",
@ -156,10 +158,14 @@
"Filter.ends-with": "ends with",
"Filter.includes": "includes",
"Filter.is": "is",
"Filter.is-after": "is after",
"Filter.is-before": "is before",
"Filter.is-empty": "is empty",
"Filter.is-not-empty": "is not empty",
"Filter.is-not-set": "is not set",
"Filter.is-set": "is set",
"Filter.isafter": "is after",
"Filter.isbefore": "is before",
"Filter.not-contains": "doesn't contain",
"Filter.not-ends-with": "doesn't end with",
"Filter.not-includes": "doesn't include",
@ -306,6 +312,7 @@
"ValueSelector.valueSelector": "Value selector",
"ValueSelectorLabel.openMenu": "Open menu",
"VersionMessage.help": "Check out what's new in this version.",
"VersionMessage.learn-more": "Learn more",
"View.AddView": "Add view",
"View.Board": "Board",
"View.DeleteView": "Delete view",
@ -360,6 +367,9 @@
"WelcomePage.StartUsingIt.Text": "Start using it",
"Workspace.editing-board-template": "You're editing a board template.",
"badge.guest": "Guest",
"boardPage.confirm-join-button": "Join",
"boardPage.confirm-join-text": "You are about to join a private board without explicitly being added by the board admin. Are you sure you wish to join this private board?",
"boardPage.confirm-join-title": "Join private board",
"boardSelector.confirm-link-board": "Link board to channel",
"boardSelector.confirm-link-board-button": "Yes, link board",
"boardSelector.confirm-link-board-subtext": "When you link \"{boardName}\" to the channel, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests. You can unlink a board from a channel at any time.",

View File

@ -1,73 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/messages/CloudMessage not plugin mode, close message 1`] = `<div />`;
exports[`components/messages/CloudMessage not plugin mode, show message, close message 1`] = `
<div>
<div
class="CloudMessage"
>
<div
class="banner"
>
<i
class="CompassIcon icon-information-outline CompassIcon"
/>
Get your own free cloud server.
<button
title="Learn more"
type="button"
>
<span>
Learn more
</span>
</button>
</div>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
</div>
`;
exports[`components/messages/CloudMessage not plugin mode, single user, close message 1`] = `
<div>
<div
class="CloudMessage"
>
<div
class="banner"
>
<i
class="CompassIcon icon-information-outline CompassIcon"
/>
Get your own free cloud server.
<button
title="Learn more"
type="button"
>
<span>
Learn more
</span>
</button>
</div>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
</div>
`;
exports[`components/messages/CloudMessage plugin mode, no display 1`] = `<div />`;

View File

@ -1,38 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
.CloudMessage {
background-color: rgb(var(--sidebar-text-active-border-rgb));
display: flex;
flex-direction: row;
align-items: center;
text-align: center;
font-weight: 600;
div {
width: 100%;
}
> .banner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 10px;
color: #fff;
.CompassIcon {
font-size: 18px;
margin-right: 2px;
}
.Button {
margin-left: 8px;
background-color: rgba(255, 255, 255, 0.16);
}
}
.IconButton {
float: right;
color: #fff;
}
}

View File

@ -1,190 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {Provider as ReduxProvider} from 'react-redux'
import {render, screen} from '@testing-library/react'
import {mocked} from 'jest-mock'
import userEvent from '@testing-library/user-event'
import configureStore from 'redux-mock-store'
import {Utils} from '../../utils'
import {IUser} from '../../user'
import {wrapIntl} from '../../testUtils'
import client from '../../octoClient'
import {UserSettings} from '../../userSettings'
import CloudMessage from './cloudMessage'
jest.mock('../../utils')
jest.mock('../../octoClient')
const mockedOctoClient = mocked(client, true)
describe('components/messages/CloudMessage', () => {
beforeEach(() => {
jest.clearAllMocks()
})
const mockedUtils = mocked(Utils, true)
const mockStore = configureStore([])
test('plugin mode, no display', () => {
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
const me: IUser = {
id: 'user-id-1',
username: 'username_1',
email: '',
nickname: '',
firstname: '',
lastname: '',
props: {},
create_at: 0,
update_at: 0,
is_bot: false,
is_guest: false,
roles: 'system_user',
}
const state = {
users: {
me,
},
}
const store = mockStore(state)
const component = wrapIntl(
<ReduxProvider store={store}>
<CloudMessage/>
</ReduxProvider>,
)
const {container} = render(component)
expect(container).toMatchSnapshot()
})
test('not plugin mode, close message', () => {
const me: IUser = {
id: 'user-id-1',
username: 'username_1',
email: '',
nickname: '',
firstname: '',
lastname: '',
create_at: 0,
update_at: 0,
is_bot: false,
is_guest: false,
roles: 'system_user',
props: {},
}
const state = {
users: {
me,
myConfig: {
cloudMessageCanceled: {value: 'true'},
},
},
}
const store = mockStore(state)
mockedUtils.isFocalboardPlugin.mockReturnValue(false)
const component = wrapIntl(
<ReduxProvider store={store}>
<CloudMessage/>
</ReduxProvider>,
)
const {container} = render(component)
expect(container).toMatchSnapshot()
})
test('not plugin mode, show message, close message', () => {
const me: IUser = {
id: 'user-id-1',
username: 'username_1',
email: '',
nickname: '',
firstname: '',
lastname: '',
props: {},
create_at: 0,
update_at: 0,
is_bot: false,
is_guest: false,
roles: 'system_user',
}
const state = {
users: {
me,
},
}
const store = mockStore(state)
mockedUtils.isFocalboardPlugin.mockReturnValue(false)
const component = wrapIntl(
<ReduxProvider store={store}>
<CloudMessage/>
</ReduxProvider>,
)
const {container} = render(component)
expect(container).toMatchSnapshot()
const buttonElement = screen.getByRole('button', {name: 'Close dialog'})
userEvent.click(buttonElement)
expect(mockedOctoClient.patchUserConfig).toBeCalledWith('user-id-1', {
updatedFields: {
cloudMessageCanceled: 'true',
},
})
})
test('not plugin mode, single user, close message', () => {
const me: IUser = {
id: 'single-user',
username: 'single-user',
email: 'single-user',
nickname: '',
firstname: '',
lastname: '',
props: {},
create_at: 0,
update_at: Date.now() - (1000 * 60 * 60 * 24), //24 hours,
is_bot: false,
is_guest: false,
roles: 'system_user',
}
const state = {
users: {
me,
},
}
const store = mockStore(state)
const hideCloudMessageSpy = jest.spyOn(UserSettings, 'hideCloudMessage', 'set')
mockedUtils.isFocalboardPlugin.mockReturnValue(false)
const component = wrapIntl(
<ReduxProvider store={store}>
<CloudMessage/>
</ReduxProvider>,
)
const {container} = render(component)
expect(container).toMatchSnapshot()
const buttonElement = screen.getByRole('button', {name: 'Close dialog'})
userEvent.click(buttonElement)
expect(mockedOctoClient.patchUserConfig).toBeCalledTimes(0)
expect(hideCloudMessageSpy).toHaveBeenCalledWith(true)
expect(UserSettings.hideCloudMessage).toBe(true)
})
})

View File

@ -1,114 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {useIntl, FormattedMessage} from 'react-intl'
import {Utils} from '../../utils'
import IconButton from '../../widgets/buttons/iconButton'
import Button from '../../widgets/buttons/button'
import CloseIcon from '../../widgets/icons/close'
import {useAppSelector, useAppDispatch} from '../../store/hooks'
import octoClient from '../../octoClient'
import {IUser, UserConfigPatch} from '../../user'
import {getMe, patchProps, getCloudMessageCanceled} from '../../store/users'
import {UserSettings} from '../../userSettings'
import CompassIcon from '../../widgets/icons/compassIcon'
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../telemetry/telemetryClient'
import './cloudMessage.scss'
const signupURL = 'https://mattermost.com/pricing'
const displayAfter = (1000 * 60 * 60 * 24) //24 hours
const CloudMessage = React.memo(() => {
const intl = useIntl()
const dispatch = useAppDispatch()
const me = useAppSelector<IUser|null>(getMe)
const cloudMessageCanceled = useAppSelector(getCloudMessageCanceled)
const closeDialogText = intl.formatMessage({
id: 'Dialog.closeDialog',
defaultMessage: 'Close dialog',
})
const onClose = async () => {
if (me) {
if (me.id === 'single-user') {
UserSettings.hideCloudMessage = true
dispatch(patchProps([
{
user_id: me.id,
category: 'focalboard',
name: 'cloudMessageCanceled',
value: 'true',
},
]))
return
}
const patch: UserConfigPatch = {
updatedFields: {
cloudMessageCanceled: 'true',
},
}
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
if (patchedProps) {
dispatch(patchProps(patchedProps))
}
}
}
if (Utils.isFocalboardPlugin() || cloudMessageCanceled) {
return null
}
if (me) {
const installTime = Date.now() - me.create_at
if (installTime < displayAfter) {
return null
}
}
return (
<div className='CloudMessage'>
<div className='banner'>
<CompassIcon
icon='information-outline'
className='CompassIcon'
/>
<FormattedMessage
id='CloudMessage.cloud-server'
defaultMessage='Get your own free cloud server.'
/>
<Button
title='Learn more'
size='xsmall'
emphasis='primary'
onClick={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CloudMoreInfo)
window.open(signupURL)
}}
>
<FormattedMessage
id='cloudMessage.learn-more'
defaultMessage='Learn more'
/>
</Button>
</div>
<IconButton
className='margin-right'
onClick={onClose}
icon={<CloseIcon/>}
title={closeDialogText}
size='small'
/>
</div>
)
})
export default CloudMessage

View File

@ -71,7 +71,7 @@ const VersionMessage = React.memo(() => {
}}
>
<FormattedMessage
id='cloudMessage.learn-more'
id='VersionMessage.learn-more'
defaultMessage='Learn more'
/>
</Button>

View File

@ -1,7 +1,6 @@
.BoardPage {
position: relative;
.CloudMessage:not(:first-child),
.VersionMessage:not(:first-child) {
position: absolute;
top: 0;

View File

@ -6,7 +6,6 @@ import {FormattedMessage, useIntl} from 'react-intl'
import {useRouteMatch, useHistory} from 'react-router-dom'
import Workspace from '../../components/workspace'
import CloudMessage from '../../components/messages/cloudMessage'
import VersionMessage from '../../components/messages/versionMessage'
import octoClient from '../../octoClient'
import {Subscription, WSClient} from '../../wsclient'
@ -300,7 +299,6 @@ const BoardPage = (props: Props): JSX.Element => {
<SetWindowTitleAndIcon/>
<UndoRedoHotKeys/>
<WebsocketConnection/>
<CloudMessage/>
<VersionMessage/>
{!mobileWarningClosed &&

View File

@ -10,10 +10,6 @@ import {Utils} from '../utils'
import {Subscription} from '../wsclient'
// TODO: change this whene the initial load is complete
// import {initialLoad} from './initialLoad'
import {UserSettings} from '../userSettings'
import {initialLoad} from './initialLoad'
import {RootState} from './index'
@ -169,20 +165,6 @@ export const getOnboardingTourCategory = createSelector(
(myConfig): string => (myConfig.tourCategory ? myConfig.tourCategory.value : ''),
)
export const getCloudMessageCanceled = createSelector(
getMe,
getMyConfig,
(me, myConfig): boolean => {
if (!me) {
return false
}
if (me.id === 'single-user') {
return UserSettings.hideCloudMessage
}
return Boolean(myConfig.cloudMessageCanceled?.value)
},
)
export const getVersionMessageCanceled = createSelector(
getMe,
getMyConfig,

View File

@ -17,7 +17,6 @@ export enum UserSettingKey {
RandomIcons = 'randomIcons',
MobileWarningClosed = 'mobileWarningClosed',
WelcomePageViewed = 'welcomePageViewed',
HideCloudMessage = 'hideCloudMessage',
NameFormat = 'nameFormat'
}
@ -149,14 +148,6 @@ export class UserSettings {
UserSettings.set(UserSettingKey.MobileWarningClosed, String(newValue))
}
static get hideCloudMessage(): boolean {
return localStorage.getItem(UserSettingKey.HideCloudMessage) === 'true'
}
static set hideCloudMessage(newValue: boolean) {
localStorage.setItem(UserSettingKey.HideCloudMessage, JSON.stringify(newValue))
}
static get nameFormat(): string | null {
return UserSettings.get(UserSettingKey.NameFormat)
}