1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-11-27 08:31:20 +02:00

fix merge conflict

This commit is contained in:
wiggin77 2023-01-24 16:25:23 -05:00
commit 7ebf861f1c
100 changed files with 1645 additions and 393 deletions

View File

@ -48,7 +48,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: "Test server: ${{matrix['db']}}"
run: cd focalboard; make server-test-${{matrix['db']}}
@ -83,7 +83,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: Setup Node
uses: actions/setup-node@v3
@ -137,7 +137,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: "Test server (minimum): ${{matrix['db']}}"
run: cd focalboard; make server-test-mini-${{matrix['db']}}
@ -174,7 +174,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: "Test server (minimum): ${{matrix['db']}}"
run: cd focalboard; make server-test-mini-${{matrix['db']}}

View File

@ -54,7 +54,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: Setup Node
uses: actions/setup-node@v3
@ -129,7 +129,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: List Xcode versions
run: ls -n /Applications/ | grep Xcode*
@ -190,7 +190,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: Setup NuGet
uses: nuget/setup-nuget@v1
@ -258,7 +258,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: Set up Node
uses: actions/setup-node@v3

View File

@ -30,7 +30,7 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- uses: actions/checkout@v3
with:
path: "focalboard"
@ -50,7 +50,7 @@ jobs:
path: "mattermost-server"
ref : "master"
- 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.46.2
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
run: |
cd focalboard

View File

@ -50,7 +50,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: Setup Node
uses: actions/setup-node@v3
@ -126,7 +126,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: List Xcode versions
run: ls -n /Applications/ | grep Xcode*
@ -188,7 +188,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: Setup NuGet
uses: nuget/setup-nuget@v1
@ -257,7 +257,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18.1
go-version: 1.19.5
- name: Set up Node
uses: actions/setup-node@v3

View File

@ -4,7 +4,7 @@ stages:
variables:
BUILD: "yes"
IMAGE_BUILDER: $CI_REGISTRY/mattermost/ci/images/builder:go-1.18.1-node-16.15.0-1
IMAGE_BUILDER: $CI_REGISTRY/mattermost/ci/images/builder:go-1.19.5-node-16.15.0-1
include:
- project: mattermost/ci/focalboard

View File

@ -51,7 +51,7 @@ func makeGoWork(ci bool) string {
var b strings.Builder
b.WriteString("go 1.18\n\n")
b.WriteString("go 1.19\n\n")
b.WriteString("use ./server\n")
for repo, envVarName := range repos {

View File

@ -1,6 +1,6 @@
module github.com/mattermost/focalboard/linux
go 1.18
go 1.19
replace github.com/mattermost/focalboard/server => ../server

View File

@ -1,6 +1,6 @@
module github.com/mattermost/mattermost-plugin-starter-template/build
go 1.18
go 1.19
require (
github.com/go-git/go-git/v5 v5.1.0

View File

@ -1,6 +1,6 @@
module github.com/mattermost/focalboard/mattermost-plugin
go 1.18
go 1.19
require (
github.com/golang/mock v1.6.0

View File

@ -27,10 +27,7 @@ linters:
enable:
- gofmt
- goimports
- deadcode
- ineffassign
- structcheck
- varcheck
- unparam
- errcheck
- govet

View File

@ -221,7 +221,7 @@ func stringResponse(w http.ResponseWriter, message string) {
_, _ = fmt.Fprint(w, message)
}
func jsonStringResponse(w http.ResponseWriter, code int, message string) {
func jsonStringResponse(w http.ResponseWriter, code int, message string) { //nolint:unparam
setResponseHeader(w, "Content-Type", "application/json")
w.WriteHeader(code)
fmt.Fprint(w, message)
@ -233,7 +233,7 @@ func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) {
_, _ = w.Write(json)
}
func setResponseHeader(w http.ResponseWriter, key string, value string) {
func setResponseHeader(w http.ResponseWriter, key string, value string) { //nolint:unparam
header := w.Header()
if header == nil {
return

View File

@ -8,7 +8,7 @@ import (
)
// makeAuditRecord creates an audit record pre-populated with data from the request.
func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus string) *audit.Record {
func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus string) *audit.Record { //nolint:unparam
ctx := r.Context()
var sessionID string
var userID string

View File

@ -383,12 +383,13 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request)
authService := session.AuthService
if authService != a.authService {
a.logger.Error(`Session authService mismatch`,
msg := `Session authService mismatch`
a.logger.Error(msg,
mlog.String("sessionID", session.ID),
mlog.String("want", a.authService),
mlog.String("got", authService),
)
a.errorResponse(w, r, model.NewErrUnauthorized(err.Error()))
a.errorResponse(w, r, model.NewErrUnauthorized(msg))
return
}

View File

@ -20,6 +20,8 @@ func (a *API) registerCategoriesRoutes(r *mux.Router) {
r.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleGetUserCategoryBoards)).Methods(http.MethodGet)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/reorder", a.sessionRequired(a.handleReorderCategoryBoards)).Methods(http.MethodPut)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}", a.sessionRequired(a.handleUpdateCategoryBoard)).Methods(http.MethodPost)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide", a.sessionRequired(a.handleHideBoard)).Methods(http.MethodPut)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}/unhide", a.sessionRequired(a.handleUnhideBoard)).Methods(http.MethodPut)
}
func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
@ -355,7 +357,7 @@ func (a *API) handleUpdateCategoryBoard(w http.ResponseWriter, r *http.Request)
userID := session.UserID
// TODO: Check the category and the team matches
err := a.app.AddUpdateUserCategoryBoard(teamID, userID, map[string]string{boardID: categoryID})
err := a.app.AddUpdateUserCategoryBoard(teamID, userID, categoryID, []string{boardID})
if err != nil {
a.errorResponse(w, r, err)
return
@ -521,3 +523,125 @@ func (a *API) handleReorderCategoryBoards(w http.ResponseWriter, r *http.Request
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleHideBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide hideBoard
//
// Hide the specified board for the user
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID to which the board to be hidden belongs to
// required: true
// type: string
// - name: boardID
// in: path
// description: ID of board to be hidden
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Category"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
vars := mux.Vars(r)
teamID := vars["teamID"]
boardID := vars["boardID"]
categoryID := vars["categoryID"]
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
auditRec := a.makeAuditRecord(r, "hideBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("board_id", boardID)
auditRec.AddMeta("team_id", teamID)
auditRec.AddMeta("category_id", categoryID)
if err := a.app.SetBoardVisibility(teamID, userID, categoryID, boardID, false); err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleUnhideBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide unhideBoard
//
// Unhides the specified board for the user
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID to which the board to be unhidden belongs to
// required: true
// type: string
// - name: boardID
// in: path
// description: ID of board to be unhidden
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Category"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
vars := mux.Vars(r)
teamID := vars["teamID"]
boardID := vars["boardID"]
categoryID := vars["categoryID"]
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
auditRec := a.makeAuditRecord(r, "unhideBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
if err := a.app.SetBoardVisibility(teamID, userID, categoryID, boardID, true); err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}

View File

@ -146,7 +146,7 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
}
if reqBoardMember.UserID == "" {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
a.errorResponse(w, r, model.NewErrBadRequest("empty userID"))
return
}

View File

@ -154,8 +154,8 @@ func (a *App) setBoardCategoryFromSource(sourceBoardID, destinationBoardID, user
var destinationCategoryID string
for _, categoryBoard := range userCategoryBoards {
for _, boardID := range categoryBoard.BoardIDs {
if boardID == sourceBoardID {
for _, metadata := range categoryBoard.BoardMetadata {
if metadata.BoardID == sourceBoardID {
// category found!
destinationCategoryID = categoryBoard.ID
break
@ -175,7 +175,7 @@ func (a *App) setBoardCategoryFromSource(sourceBoardID, destinationBoardID, user
// now that we have source board's category,
// we send destination board to the same category
return a.AddUpdateUserCategoryBoard(teamID, userID, map[string]string{destinationBoardID: destinationCategoryID})
return a.AddUpdateUserCategoryBoard(teamID, userID, destinationCategoryID, []string{destinationBoardID})
}
func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
@ -329,12 +329,12 @@ func (a *App) addBoardsToDefaultCategory(userID, teamID string, boards []*model.
return fmt.Errorf("%w userID: %s", errNoDefaultCategoryFound, userID)
}
boardCategoryMapping := map[string]string{}
for _, board := range boards {
boardCategoryMapping[board.ID] = defaultCategoryID
boardIDs := make([]string, len(boards))
for i := range boards {
boardIDs[i] = boards[i].ID
}
if err := a.AddUpdateUserCategoryBoard(teamID, userID, boardCategoryMapping); err != nil {
if err := a.AddUpdateUserCategoryBoard(teamID, userID, defaultCategoryID, boardIDs); err != nil {
return err
}

View File

@ -51,8 +51,8 @@ func TestAddMemberToBoard(t *testing.T) {
Type: "system",
},
},
}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", map[string]string{"board_id_1": "default_category_id"}).Return(nil)
}, nil).Times(2)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "default_category_id", []string{"board_id_1"}).Return(nil)
addedBoardMember, err := th.App.AddMemberToBoard(boardMember)
require.NoError(t, err)
@ -125,8 +125,8 @@ func TestAddMemberToBoard(t *testing.T) {
Type: "system",
},
},
}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", map[string]string{"board_id_1": "default_category_id"}).Return(nil)
}, nil).Times(2)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "default_category_id", []string{"board_id_1"}).Return(nil)
addedBoardMember, err := th.App.AddMemberToBoard(boardMember)
require.NoError(t, err)
@ -411,45 +411,72 @@ func TestBoardCategory(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
t.Run("test addBoardsToDefaultCategory", func(t *testing.T) {
t.Run("no boards default category exists", func(t *testing.T) {
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
{
Category: model.Category{ID: "category_id_1", Name: "Category 1"},
BoardIDs: []string{"board_id_1", "board_id_2"},
t.Run("no boards default category exists", func(t *testing.T) {
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
{
Category: model.Category{ID: "category_id_1", Name: "Category 1"},
BoardMetadata: []model.CategoryBoardMetadata{
{BoardID: "board_id_1"},
{BoardID: "board_id_2"},
},
{
Category: model.Category{ID: "category_id_2", Name: "Category 2"},
BoardIDs: []string{"board_id_3"},
},
{
Category: model.Category{ID: "category_id_2", Name: "Category 2"},
BoardMetadata: []model.CategoryBoardMetadata{
{BoardID: "board_id_3"},
},
{
Category: model.Category{ID: "category_id_3", Name: "Category 3"},
BoardIDs: []string{},
},
{
Category: model.Category{ID: "category_id_3", Name: "Category 3"},
BoardMetadata: []model.CategoryBoardMetadata{},
},
}, nil).Times(1)
// when this function is called the second time, the default category is created
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
{
Category: model.Category{ID: "category_id_1", Name: "Category 1"},
BoardMetadata: []model.CategoryBoardMetadata{
{BoardID: "board_id_1"},
{BoardID: "board_id_2"},
},
}, nil)
},
{
Category: model.Category{ID: "category_id_2", Name: "Category 2"},
BoardMetadata: []model.CategoryBoardMetadata{
{BoardID: "board_id_3"},
},
},
{
Category: model.Category{ID: "category_id_3", Name: "Category 3"},
BoardMetadata: []model.CategoryBoardMetadata{},
},
{
Category: model.Category{ID: "default_category_id", Type: model.CategoryTypeSystem, Name: "Boards"},
},
}, nil).Times(1)
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil)
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
ID: "default_category_id",
Name: "Boards",
}, nil)
th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{}, nil)
th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", map[string]string{
"board_id_1": "default_category_id",
"board_id_2": "default_category_id",
"board_id_3": "default_category_id",
}).Return(nil)
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil)
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
ID: "default_category_id",
Name: "Boards",
}, nil)
th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{}, nil)
th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "default_category_id", []string{
"board_id_1",
"board_id_2",
"board_id_3",
}).Return(nil)
boards := []*model.Board{
{ID: "board_id_1"},
{ID: "board_id_2"},
{ID: "board_id_3"},
}
boards := []*model.Board{
{ID: "board_id_1"},
{ID: "board_id_2"},
{ID: "board_id_3"},
}
err := th.App.addBoardsToDefaultCategory("user_id", "team_id", boards)
assert.NoError(t, err)
})
err := th.App.addBoardsToDefaultCategory("user_id", "team_id", boards)
assert.NoError(t, err)
})
}
@ -491,9 +518,9 @@ func TestDuplicateBoard(t *testing.T) {
Type: "system",
},
},
}, nil).Times(2)
}, nil).Times(3)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", utils.Anything).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "category_id_1", utils.Anything).Return(nil)
// for WS change broadcast
th.Store.EXPECT().GetMembersForBoard(utils.Anything).Return([]*model.BoardMember{}, nil).Times(2)

View File

@ -178,13 +178,12 @@ func (a *App) moveBoardsToDefaultCategory(userID, teamID, sourceCategoryID strin
return fmt.Errorf("moveBoardsToDefaultCategory: %w", errNoDefaultCategoryFound)
}
boardCategoryMapping := map[string]string{}
for _, boardID := range sourceCategoryBoards.BoardIDs {
boardCategoryMapping[boardID] = defaultCategoryID
boardIDs := make([]string, len(sourceCategoryBoards.BoardMetadata))
for i := range sourceCategoryBoards.BoardMetadata {
boardIDs[i] = sourceCategoryBoards.BoardMetadata[i].BoardID
}
if err := a.AddUpdateUserCategoryBoard(teamID, userID, boardCategoryMapping); err != nil {
if err := a.AddUpdateUserCategoryBoard(teamID, userID, defaultCategoryID, boardIDs); err != nil {
return fmt.Errorf("moveBoardsToDefaultCategory: %w", err)
}

View File

@ -79,8 +79,8 @@ func (a *App) createBoardsCategory(userID, teamID string, existingCategoryBoards
}
createdCategoryBoards := &model.CategoryBoards{
Category: *createdCategory,
BoardIDs: []string{},
Category: *createdCategory,
BoardMetadata: []model.CategoryBoardMetadata{},
}
// get user's current team's baords
@ -89,6 +89,8 @@ func (a *App) createBoardsCategory(userID, teamID string, existingCategoryBoards
return nil, fmt.Errorf("createBoardsCategory error fetching user's team's boards: %w", err)
}
boardIDsToAdd := []string{}
for _, board := range userTeamBoards {
boardMembership, ok := boardMemberByBoardID[board.ID]
if !ok {
@ -107,8 +109,8 @@ func (a *App) createBoardsCategory(userID, teamID string, existingCategoryBoards
belongsToCategory := false
for _, categoryBoard := range existingCategoryBoards {
for _, boardID := range categoryBoard.BoardIDs {
if boardID == board.ID {
for _, metadata := range categoryBoard.BoardMetadata {
if metadata.BoardID == board.ID {
belongsToCategory = true
break
}
@ -122,29 +124,58 @@ func (a *App) createBoardsCategory(userID, teamID string, existingCategoryBoards
}
if !belongsToCategory {
if err := a.AddUpdateUserCategoryBoard(teamID, userID, map[string]string{board.ID: createdCategory.ID}); err != nil {
return nil, fmt.Errorf("createBoardsCategory failed to add category-less board to the default category, defaultCategoryID: %s, error: %w", createdCategory.ID, err)
boardIDsToAdd = append(boardIDsToAdd, board.ID)
newBoardMetadata := model.CategoryBoardMetadata{
BoardID: board.ID,
Hidden: false,
}
createdCategoryBoards.BoardMetadata = append(createdCategoryBoards.BoardMetadata, newBoardMetadata)
}
}
createdCategoryBoards.BoardIDs = append(createdCategoryBoards.BoardIDs, board.ID)
if len(boardIDsToAdd) > 0 {
if err := a.AddUpdateUserCategoryBoard(teamID, userID, createdCategory.ID, boardIDsToAdd); err != nil {
return nil, fmt.Errorf("createBoardsCategory failed to add category-less board to the default category, defaultCategoryID: %s, error: %w", createdCategory.ID, err)
}
}
return createdCategoryBoards, nil
}
func (a *App) AddUpdateUserCategoryBoard(teamID, userID string, boardCategoryMapping map[string]string) error {
err := a.store.AddUpdateCategoryBoard(userID, boardCategoryMapping)
func (a *App) AddUpdateUserCategoryBoard(teamID, userID, categoryID string, boardIDs []string) error {
if len(boardIDs) == 0 {
return nil
}
err := a.store.AddUpdateCategoryBoard(userID, categoryID, boardIDs)
if err != nil {
return err
}
wsPayload := make([]*model.BoardCategoryWebsocketData, len(boardCategoryMapping))
userCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID)
if err != nil {
return err
}
var updatedCategory *model.CategoryBoards
for i := range userCategoryBoards {
if userCategoryBoards[i].ID == categoryID {
updatedCategory = &userCategoryBoards[i]
break
}
}
if updatedCategory == nil {
return errCategoryNotFound
}
wsPayload := make([]*model.BoardCategoryWebsocketData, len(updatedCategory.BoardMetadata))
i := 0
for boardID, categoryID := range boardCategoryMapping {
for _, categoryBoardMetadata := range updatedCategory.BoardMetadata {
wsPayload[i] = &model.BoardCategoryWebsocketData{
BoardID: boardID,
BoardID: categoryBoardMetadata.BoardID,
CategoryID: categoryID,
Hidden: categoryBoardMetadata.Hidden,
}
i++
}
@ -198,12 +229,12 @@ func (a *App) verifyNewCategoryBoardsMatchExisting(userID, teamID, categoryID st
return fmt.Errorf("%w categoryID: %s", errCategoryNotFound, categoryID)
}
if len(targetCategoryBoards.BoardIDs) != len(newBoardsOrder) {
if len(targetCategoryBoards.BoardMetadata) != len(newBoardsOrder) {
return fmt.Errorf(
"%w length new category boards: %d, length existing category boards: %d, userID: %s, teamID: %s, categoryID: %s",
errCategoryBoardsLengthMismatch,
len(newBoardsOrder),
len(targetCategoryBoards.BoardIDs),
len(targetCategoryBoards.BoardMetadata),
userID,
teamID,
categoryID,
@ -211,8 +242,8 @@ func (a *App) verifyNewCategoryBoardsMatchExisting(userID, teamID, categoryID st
}
existingBoardMap := map[string]bool{}
for _, boardID := range targetCategoryBoards.BoardIDs {
existingBoardMap[boardID] = true
for _, metadata := range targetCategoryBoards.BoardMetadata {
existingBoardMap[metadata.BoardID] = true
}
for _, boardID := range newBoardsOrder {
@ -230,3 +261,19 @@ func (a *App) verifyNewCategoryBoardsMatchExisting(userID, teamID, categoryID st
return nil
}
func (a *App) SetBoardVisibility(teamID, userID, categoryID, boardID string, visible bool) error {
if err := a.store.SetBoardVisibility(userID, categoryID, boardID, visible); err != nil {
return fmt.Errorf("SetBoardVisibility: failed to update board visibility: %w", err)
}
a.wsAdapter.BroadcastCategoryBoardChange(teamID, userID, []*model.BoardCategoryWebsocketData{
{
BoardID: boardID,
CategoryID: categoryID,
Hidden: !visible,
},
})
return nil
}

View File

@ -14,7 +14,16 @@ func TestGetUserCategoryBoards(t *testing.T) {
defer tearDown()
t.Run("user had no default category and had boards", func(t *testing.T) {
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{}, nil)
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{}, nil).Times(1)
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
{
Category: model.Category{
ID: "boards_category_id",
Type: model.CategoryTypeSystem,
Name: "Boards",
},
},
}, nil).Times(1)
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil)
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
ID: "boards_category_id",
@ -49,18 +58,16 @@ func TestGetUserCategoryBoards(t *testing.T) {
Synthetic: false,
},
}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", map[string]string{"board_id_1": "boards_category_id"}).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", map[string]string{"board_id_2": "boards_category_id"}).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", map[string]string{"board_id_3": "boards_category_id"}).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil)
categoryBoards, err := th.App.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, "Boards", categoryBoards[0].Name)
assert.Equal(t, 3, len(categoryBoards[0].BoardIDs))
assert.Contains(t, categoryBoards[0].BoardIDs, "board_id_1")
assert.Contains(t, categoryBoards[0].BoardIDs, "board_id_2")
assert.Contains(t, categoryBoards[0].BoardIDs, "board_id_3")
assert.Equal(t, 3, len(categoryBoards[0].BoardMetadata))
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_1", Hidden: false})
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_2", Hidden: false})
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_3", Hidden: false})
})
t.Run("user had no default category BUT had no boards", func(t *testing.T) {
@ -78,14 +85,17 @@ func TestGetUserCategoryBoards(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, "Boards", categoryBoards[0].Name)
assert.Equal(t, 0, len(categoryBoards[0].BoardIDs))
assert.Equal(t, 0, len(categoryBoards[0].BoardMetadata))
})
t.Run("user already had a default Boards category with boards in it", func(t *testing.T) {
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
{
Category: model.Category{Name: "Boards"},
BoardIDs: []string{"board_id_1", "board_id_2"},
BoardMetadata: []model.CategoryBoardMetadata{
{BoardID: "board_id_1", Hidden: false},
{BoardID: "board_id_2", Hidden: false},
},
},
}, nil)
@ -93,7 +103,7 @@ func TestGetUserCategoryBoards(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, "Boards", categoryBoards[0].Name)
assert.Equal(t, 2, len(categoryBoards[0].BoardIDs))
assert.Equal(t, 2, len(categoryBoards[0].BoardMetadata))
})
}
@ -116,7 +126,7 @@ func TestCreateBoardsCategory(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, boardsCategory)
assert.Equal(t, "Boards", boardsCategory.Name)
assert.Equal(t, 0, len(boardsCategory.BoardIDs))
assert.Equal(t, 0, len(boardsCategory.BoardMetadata))
})
t.Run("user has implicit access to some board", func(t *testing.T) {
@ -150,7 +160,7 @@ func TestCreateBoardsCategory(t *testing.T) {
// there should still be no boards in the default category as
// the user had only implicit access to boards
assert.Equal(t, 0, len(boardsCategory.BoardIDs))
assert.Equal(t, 0, len(boardsCategory.BoardMetadata))
})
t.Run("user has explicit access to some board", func(t *testing.T) {
@ -185,9 +195,17 @@ func TestCreateBoardsCategory(t *testing.T) {
Synthetic: false,
},
}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", map[string]string{"board_id_1": "boards_category_id"}).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", map[string]string{"board_id_2": "boards_category_id"}).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", map[string]string{"board_id_3": "boards_category_id"}).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil)
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
{
Category: model.Category{
Type: model.CategoryTypeSystem,
ID: "boards_category_id",
Name: "Boards",
},
},
}, nil)
existingCategoryBoards := []model.CategoryBoards{}
boardsCategory, err := th.App.createBoardsCategory("user_id", "team_id", existingCategoryBoards)
@ -197,7 +215,7 @@ func TestCreateBoardsCategory(t *testing.T) {
// since user has explicit access to three boards,
// they should all end up in the default category
assert.Equal(t, 3, len(boardsCategory.BoardIDs))
assert.Equal(t, 3, len(boardsCategory.BoardMetadata))
})
t.Run("user has both implicit and explicit access to some board", func(t *testing.T) {
@ -226,7 +244,17 @@ func TestCreateBoardsCategory(t *testing.T) {
Synthetic: true,
},
}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", map[string]string{"board_id_1": "boards_category_id"}).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1"}).Return(nil)
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
{
Category: model.Category{
Type: model.CategoryTypeSystem,
ID: "boards_category_id",
Name: "Boards",
},
},
}, nil)
existingCategoryBoards := []model.CategoryBoards{}
boardsCategory, err := th.App.createBoardsCategory("user_id", "team_id", existingCategoryBoards)
@ -237,7 +265,7 @@ func TestCreateBoardsCategory(t *testing.T) {
// there was only one explicit board access,
// and so only that one should end up in the
// default category
assert.Equal(t, 1, len(boardsCategory.BoardIDs))
assert.Equal(t, 1, len(boardsCategory.BoardMetadata))
})
}
@ -249,15 +277,20 @@ func TestReorderCategoryBoards(t *testing.T) {
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
{
Category: model.Category{ID: "category_id_1", Name: "Category 1"},
BoardIDs: []string{"board_id_1", "board_id_2"},
BoardMetadata: []model.CategoryBoardMetadata{
{BoardID: "board_id_1", Hidden: false},
{BoardID: "board_id_2", Hidden: false},
},
},
{
Category: model.Category{ID: "category_id_2", Name: "Boards", Type: "system"},
BoardIDs: []string{"board_id_3"},
BoardMetadata: []model.CategoryBoardMetadata{
{BoardID: "board_id_3", Hidden: false},
},
},
{
Category: model.Category{ID: "category_id_3", Name: "Category 3"},
BoardIDs: []string{},
Category: model.Category{ID: "category_id_3", Name: "Category 3"},
BoardMetadata: []model.CategoryBoardMetadata{},
},
}, nil)
@ -274,15 +307,21 @@ func TestReorderCategoryBoards(t *testing.T) {
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
{
Category: model.Category{ID: "category_id_1", Name: "Category 1"},
BoardIDs: []string{"board_id_1", "board_id_2", "board_id_3"},
BoardMetadata: []model.CategoryBoardMetadata{
{BoardID: "board_id_1", Hidden: false},
{BoardID: "board_id_2", Hidden: false},
{BoardID: "board_id_3", Hidden: false},
},
},
{
Category: model.Category{ID: "category_id_2", Name: "Boards", Type: "system"},
BoardIDs: []string{"board_id_3"},
BoardMetadata: []model.CategoryBoardMetadata{
{BoardID: "board_id_3", Hidden: false},
},
},
{
Category: model.Category{ID: "category_id_3", Name: "Category 3"},
BoardIDs: []string{},
Category: model.Category{ID: "category_id_3", Name: "Category 3"},
BoardMetadata: []model.CategoryBoardMetadata{},
},
}, nil)

View File

@ -278,7 +278,7 @@ func TestDeleteCategory(t *testing.T) {
Type: "default",
Name: "Boards",
},
BoardIDs: []string{},
BoardMetadata: []model.CategoryBoardMetadata{},
},
{
Category: model.Category{
@ -289,12 +289,10 @@ func TestDeleteCategory(t *testing.T) {
Type: "custom",
Name: "Category 1",
},
BoardIDs: []string{},
BoardMetadata: []model.CategoryBoardMetadata{},
},
}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", utils.Anything).Return(nil)
deletedCategory, err := th.App.DeleteCategory("category_id_1", "user_id_1", "team_id_1")
assert.NotNil(t, deletedCategory)
assert.NoError(t, err)
@ -351,8 +349,6 @@ func TestMoveBoardsToDefaultCategory(t *testing.T) {
},
}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", utils.Anything).Return(nil)
err := th.App.moveBoardsToDefaultCategory("user_id", "team_id", "category_id_2")
assert.NoError(t, err)
})
@ -376,7 +372,6 @@ func TestMoveBoardsToDefaultCategory(t *testing.T) {
}, nil)
th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{}, nil)
th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", utils.Anything).Return(nil)
err := th.App.moveBoardsToDefaultCategory("user_id", "team_id", "category_id_2")
assert.NoError(t, err)

View File

@ -47,6 +47,18 @@ func TestApp_ImportArchive(t *testing.T) {
th.Store.EXPECT().CreateBoardsAndBlocks(gomock.AssignableToTypeOf(&model.BoardsAndBlocks{}), "user").Return(babs, nil)
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{boardMember}, nil)
// th.Store.EXPECT().GetBoard(board.ID).Return(board, nil)
// th.Store.EXPECT().GetMemberForBoard(board.ID, "user").Return(boardMember, nil)
// th.Store.EXPECT().GetUserCategoryBoards("user", "test-team").Return([]model.CategoryBoards{}, nil)
th.Store.EXPECT().GetUserCategoryBoards("user", "test-team").Return([]model.CategoryBoards{
{
Category: model.Category{
Type: "default",
Name: "Boards",
ID: "boards_category_id",
},
},
}, nil)
th.Store.EXPECT().GetUserCategoryBoards("user", "test-team")
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil)
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
@ -55,7 +67,7 @@ func TestApp_ImportArchive(t *testing.T) {
}, nil)
th.Store.EXPECT().GetBoardsForUserAndTeam("user", "test-team", false).Return([]*model.Board{}, nil)
th.Store.EXPECT().GetMembersForUser("user").Return([]*model.BoardMember{}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user", utils.Anything).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user", utils.Anything, utils.Anything).Return(nil)
err := th.App.ImportArchive(r, opts)
require.NoError(t, err, "import archive should not fail")
@ -97,7 +109,16 @@ func TestApp_ImportArchive(t *testing.T) {
th.Store.EXPECT().CreateBoardsAndBlocks(gomock.AssignableToTypeOf(&model.BoardsAndBlocks{}), "f1tydgc697fcbp8ampr6881jea").Return(babs, nil)
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{bm1, bm2, bm3}, nil)
th.Store.EXPECT().GetUserCategoryBoards("f1tydgc697fcbp8ampr6881jea", "test-team")
th.Store.EXPECT().GetUserCategoryBoards("f1tydgc697fcbp8ampr6881jea", "test-team").Return([]model.CategoryBoards{}, nil)
th.Store.EXPECT().GetUserCategoryBoards("f1tydgc697fcbp8ampr6881jea", "test-team").Return([]model.CategoryBoards{
{
Category: model.Category{
ID: "boards_category_id",
Name: "Boards",
Type: model.CategoryTypeSystem,
},
},
}, nil)
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil)
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
ID: "boards_category_id",
@ -105,7 +126,7 @@ func TestApp_ImportArchive(t *testing.T) {
}, nil)
th.Store.EXPECT().GetMembersForUser("f1tydgc697fcbp8ampr6881jea").Return([]*model.BoardMember{}, nil)
th.Store.EXPECT().GetBoardsForUserAndTeam("f1tydgc697fcbp8ampr6881jea", "test-team", false).Return([]*model.Board{}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("f1tydgc697fcbp8ampr6881jea", utils.Anything).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("f1tydgc697fcbp8ampr6881jea", utils.Anything, utils.Anything).Return(nil)
th.Store.EXPECT().GetBoard(board.ID).AnyTimes().Return(board, nil)
th.Store.EXPECT().GetMemberForBoard(board.ID, "f1tydgc697fcbp8ampr6881jea").AnyTimes().Return(bm1, nil)
th.Store.EXPECT().GetMemberForBoard(board.ID, "hxxzooc3ff8cubsgtcmpn8733e").AnyTimes().Return(bm2, nil)

View File

@ -19,7 +19,7 @@ func (a *App) GetTeamBoardsInsights(userID string, teamID string, opts *mmModel.
if err != nil {
return nil, err
}
return a.store.GetTeamBoardsInsights(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage, boardIDs)
return a.store.GetTeamBoardsInsights(teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage, boardIDs)
}
func (a *App) GetUserBoardsInsights(userID string, teamID string, opts *mmModel.InsightsOpts) (*model.BoardInsightsList, error) {

View File

@ -51,7 +51,7 @@ func TestGetTeamAndUserBoardsInsights(t *testing.T) {
th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes()
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id", true).Return(mockInsightsBoards, nil).AnyTimes()
th.Store.EXPECT().
GetTeamBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
GetTeamBoardsInsights("team-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
Return(mockTeamInsightsList, nil)
results, err := th.App.GetTeamBoardsInsights("user-id", "team-id", &mmModel.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 10})
require.NoError(t, err)
@ -74,7 +74,7 @@ func TestGetTeamAndUserBoardsInsights(t *testing.T) {
th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes()
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id", true).Return(mockInsightsBoards, nil).AnyTimes()
th.Store.EXPECT().
GetTeamBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
GetTeamBoardsInsights("team-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
Return(nil, insightError{"board-insight-error"})
_, err := th.App.GetTeamBoardsInsights("user-id", "team-id", &mmModel.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 10})
require.Error(t, err)

View File

@ -70,7 +70,7 @@ func TestPrepareOnboardingTour(t *testing.T) {
{
Category: model.Category{ID: "boards_category_id", Name: "Boards"},
},
}, nil).Times(1)
}, nil).Times(2)
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil).Times(1)
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
@ -78,7 +78,7 @@ func TestPrepareOnboardingTour(t *testing.T) {
Name: "Boards",
}, nil)
th.Store.EXPECT().GetBoardsForUserAndTeam("user_id_1", teamID, false).Return([]*model.Board{}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", map[string]string{"board_id_2": "boards_category_id"}).Return(nil)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "boards_category_id", []string{"board_id_2"}).Return(nil)
teamID, boardID, err := th.App.PrepareOnboardingTour(userID, teamID)
assert.NoError(t, err)
@ -120,8 +120,8 @@ func TestCreateWelcomeBoard(t *testing.T) {
{
Category: model.Category{ID: "boards_category_id", Name: "Boards"},
},
}, nil).Times(2)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", map[string]string{"board_id_1": "boards_category_id"}).Return(nil)
}, nil).Times(3)
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "boards_category_id", []string{"board_id_1"}).Return(nil)
boardID, err := th.App.createWelcomeBoard(userID, teamID)
assert.Nil(t, err)

View File

@ -7,5 +7,6 @@ import (
// DefaultTemplatesArchive is an embedded archive file containing the default
// templates to be imported to team 0.
// This archive is generated with `make templates-archive`
//
//go:embed templates.boardarchive
var DefaultTemplatesArchive []byte

View File

@ -1042,3 +1042,21 @@ func (c *Client) GetBlocksComplianceHistory(
return res, BuildResponse(r)
}
func (c *Client) HideBoard(teamID, categoryID, boardID string) *Response {
r, err := c.DoAPIPut(c.GetTeamRoute(teamID)+"/categories/"+categoryID+"/boards/"+boardID+"/hide", "")
if err != nil {
return BuildErrorResponse(r, err)
}
return BuildResponse(r)
}
func (c *Client) UnhideBoard(teamID, categoryID, boardID string) *Response {
r, err := c.DoAPIPut(c.GetTeamRoute(teamID)+"/categories/"+categoryID+"/boards/"+boardID+"/unhide", "")
if err != nil {
return BuildErrorResponse(r, err)
}
return BuildResponse(r)
}

View File

@ -1,6 +1,6 @@
module github.com/mattermost/focalboard/server
go 1.18
go 1.19
require (
github.com/Masterminds/squirrel v1.5.3

View File

@ -1,6 +1,6 @@
module github.com/mattermost/focalboard/server
go 1.18
go 1.19
require github.com/golang/mock v1.6.0

View File

@ -2078,8 +2078,8 @@ func TestDuplicateBoard(t *testing.T) {
var duplicateBoardCategoryID string
for _, categoryBoard := range userCategoryBoards {
for _, boardID := range categoryBoard.BoardIDs {
if boardID == duplicateBoard.ID {
for _, boardMetadata := range categoryBoard.BoardMetadata {
if boardMetadata.BoardID == duplicateBoard.ID {
duplicateBoardCategoryID = categoryBoard.Category.ID
}
}

View File

@ -269,9 +269,7 @@ func TestGetCard(t *testing.T) {
})
}
//
// Helpers.
//
func reverse(src []string) []string {
out := make([]string, 0, len(src))
for i := len(src) - 1; i >= 0; i-- {

View File

@ -18,8 +18,8 @@ func TestSidebar(t *testing.T) {
categoryBoards := th.GetUserCategoryBoards("team-id")
require.Equal(t, 1, len(categoryBoards))
require.Equal(t, "Boards", categoryBoards[0].Name)
require.Equal(t, 1, len(categoryBoards[0].BoardIDs))
require.Equal(t, board.ID, categoryBoards[0].BoardIDs[0])
require.Equal(t, 1, len(categoryBoards[0].BoardMetadata))
require.Equal(t, board.ID, categoryBoards[0].BoardMetadata[0].BoardID)
// create a new category, a new board
// and move that board into the new category
@ -39,8 +39,8 @@ func TestSidebar(t *testing.T) {
// the newly created category should be the first one array
// as new categories end up on top in LHS
require.Equal(t, "Category 2", categoryBoards[0].Name)
require.Equal(t, 1, len(categoryBoards[0].BoardIDs))
require.Equal(t, board2.ID, categoryBoards[0].BoardIDs[0])
require.Equal(t, 1, len(categoryBoards[0].BoardMetadata))
require.Equal(t, board2.ID, categoryBoards[0].BoardMetadata[0].BoardID)
// now we'll delete the custom category we created, "Category 2"
// and all it's boards should get moved to the Boards category
@ -48,7 +48,51 @@ func TestSidebar(t *testing.T) {
categoryBoards = th.GetUserCategoryBoards("team-id")
require.Equal(t, 1, len(categoryBoards))
require.Equal(t, "Boards", categoryBoards[0].Name)
require.Equal(t, 2, len(categoryBoards[0].BoardIDs))
require.Contains(t, categoryBoards[0].BoardIDs, board.ID)
require.Contains(t, categoryBoards[0].BoardIDs, board2.ID)
require.Equal(t, 2, len(categoryBoards[0].BoardMetadata))
require.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: board.ID, Hidden: false})
require.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: board2.ID, Hidden: false})
}
func TestHideUnhideBoard(t *testing.T) {
th := SetupTestHelperWithToken(t).Start()
defer th.TearDown()
// we'll create a new board.
// The board should end up in a default "Boards" category
th.CreateBoard("team-id", "O")
// the created board should not be hidden
categoryBoards := th.GetUserCategoryBoards("team-id")
require.Equal(t, 1, len(categoryBoards))
require.Equal(t, "Boards", categoryBoards[0].Name)
require.Equal(t, 1, len(categoryBoards[0].BoardMetadata))
require.False(t, categoryBoards[0].BoardMetadata[0].Hidden)
// now we'll hide the board
response := th.Client.HideBoard("team-id", categoryBoards[0].ID, categoryBoards[0].BoardMetadata[0].BoardID)
th.CheckOK(response)
// verifying if the board has been marked as hidden
categoryBoards = th.GetUserCategoryBoards("team-id")
require.True(t, categoryBoards[0].BoardMetadata[0].Hidden)
// trying to hide the already hidden board.This should have no effect
response = th.Client.HideBoard("team-id", categoryBoards[0].ID, categoryBoards[0].BoardMetadata[0].BoardID)
th.CheckOK(response)
categoryBoards = th.GetUserCategoryBoards("team-id")
require.True(t, categoryBoards[0].BoardMetadata[0].Hidden)
// now we'll unhide the board
response = th.Client.UnhideBoard("team-id", categoryBoards[0].ID, categoryBoards[0].BoardMetadata[0].BoardID)
th.CheckOK(response)
// verifying
categoryBoards = th.GetUserCategoryBoards("team-id")
require.False(t, categoryBoards[0].BoardMetadata[0].Hidden)
// trying to unhide the already visible board.This should have no effect
response = th.Client.UnhideBoard("team-id", categoryBoards[0].ID, categoryBoards[0].BoardMetadata[0].BoardID)
th.CheckOK(response)
categoryBoards = th.GetUserCategoryBoards("team-id")
require.False(t, categoryBoards[0].BoardMetadata[0].Hidden)
}

View File

@ -122,7 +122,7 @@ func main() {
if pDBConfig != nil && len(*pDBConfig) > 0 {
config.DBConfigString = *pDBConfig
// Don't echo, as the confix string may contain passwords
logger.Info("DBConfigString overriden from commandline")
logger.Info("DBConfigString overridden from commandline")
}
if pPort != nil && *pPort > 0 && *pPort != config.Port {
@ -166,6 +166,7 @@ func main() {
}
// StartServer starts the server
//
//export StartServer
func StartServer(webPath *C.char, filesPath *C.char, port int, singleUserToken, dbConfigString, configFilePath *C.char) {
startServer(
@ -179,6 +180,7 @@ func StartServer(webPath *C.char, filesPath *C.char, port int, singleUserToken,
}
// StopServer stops the server
//
//export StopServer
func StopServer() {
stopServer()

View File

@ -9,7 +9,7 @@ type CategoryBoards struct {
// The IDs of boards in this category
// required: true
BoardIDs []string `json:"boardIDs"`
BoardMetadata []CategoryBoardMetadata `json:"boardMetadata"`
// The relative sort order of this board in its category
// required: true
@ -19,4 +19,10 @@ type CategoryBoards struct {
type BoardCategoryWebsocketData struct {
BoardID string `json:"boardID"`
CategoryID string `json:"categoryID"`
Hidden bool `json:"hidden"`
}
type CategoryBoardMetadata struct {
BoardID string `json:"boardID"`
Hidden bool `json:"hidden"`
}

View File

@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate mockgen -destination=mocks/propValueResolverMock.go -package mocks . PropValueResolver
package model
import (

View File

@ -53,7 +53,7 @@ var (
// LogServerInfo logs information about the server instance.
func LogServerInfo(logger mlog.LoggerIFace) {
logger.Info("FocalBoard Server",
logger.Info("Focalboard server",
mlog.String("version", CurrentVersion),
mlog.String("edition", Edition),
mlog.String("build_number", BuildNumber),

View File

@ -377,7 +377,7 @@ func (s *Server) UpdateAppConfig() {
// Local server
func (s *Server) startLocalModeServer() error {
s.localModeServer = &http.Server{
s.localModeServer = &http.Server{ //nolint:gosec
Handler: s.localRouter,
ConnContext: api.SetContextConn,
}

View File

@ -46,12 +46,14 @@ func NewAudit(options ...mlog.Option) (*Audit, error) {
// Configure provides a new configuration for this audit service.
// Zero or more sources of config can be provided:
// cfgFile - path to file containing JSON
// cfgEscaped - JSON string probably from ENV var
//
// cfgFile - path to file containing JSON
// cfgEscaped - JSON string probably from ENV var
//
// For each case JSON containing log targets is provided. Target name collisions are resolved
// using the following precedence:
// cfgFile > cfgEscaped
//
// cfgFile > cfgEscaped
func (a *Audit) Configure(cfgFile string, cfgEscaped string) error {
return a.auditLogger.Configure(cfgFile, cfgEscaped, nil)
}

View File

@ -17,7 +17,7 @@ type Service struct {
// NewMetricsServer factory method to create a new prometheus server.
func NewMetricsServer(address string, metricsService *Metrics, logger mlog.LoggerIFace) *Service {
return &Service{
&http.Server{
&http.Server{ //nolint:gosec
Addr: address,
Handler: promhttp.HandlerFor(metricsService.registry, promhttp.HandlerOpts{
ErrorLog: logger.StdLogger(mlog.LvlError),

View File

@ -599,7 +599,7 @@ func (s *MattermostAuthLayer) GetLicense() *mmModel.License {
return s.servicesAPI.GetLicense()
}
func boardFields(prefix string) []string {
func boardFields(prefix string) []string { //nolint:unparam
fields := []string{
"id",
"team_id",

View File

@ -37,17 +37,17 @@ func (m *MockStore) EXPECT() *MockStoreMockRecorder {
}
// AddUpdateCategoryBoard mocks base method.
func (m *MockStore) AddUpdateCategoryBoard(arg0 string, arg1 map[string]string) error {
func (m *MockStore) AddUpdateCategoryBoard(arg0, arg1 string, arg2 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddUpdateCategoryBoard", arg0, arg1)
ret := m.ctrl.Call(m, "AddUpdateCategoryBoard", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// AddUpdateCategoryBoard indicates an expected call of AddUpdateCategoryBoard.
func (mr *MockStoreMockRecorder) AddUpdateCategoryBoard(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) AddUpdateCategoryBoard(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUpdateCategoryBoard", reflect.TypeOf((*MockStore)(nil).AddUpdateCategoryBoard), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUpdateCategoryBoard", reflect.TypeOf((*MockStore)(nil).AddUpdateCategoryBoard), arg0, arg1, arg2)
}
// CanSeeUser mocks base method.
@ -1047,18 +1047,18 @@ func (mr *MockStoreMockRecorder) GetTeam(arg0 interface{}) *gomock.Call {
}
// GetTeamBoardsInsights mocks base method.
func (m *MockStore) GetTeamBoardsInsights(arg0, arg1 string, arg2 int64, arg3, arg4 int, arg5 []string) (*model.BoardInsightsList, error) {
func (m *MockStore) GetTeamBoardsInsights(arg0 string, arg1 int64, arg2, arg3 int, arg4 []string) (*model.BoardInsightsList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTeamBoardsInsights", arg0, arg1, arg2, arg3, arg4, arg5)
ret := m.ctrl.Call(m, "GetTeamBoardsInsights", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(*model.BoardInsightsList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTeamBoardsInsights indicates an expected call of GetTeamBoardsInsights.
func (mr *MockStoreMockRecorder) GetTeamBoardsInsights(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetTeamBoardsInsights(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamBoardsInsights", reflect.TypeOf((*MockStore)(nil).GetTeamBoardsInsights), arg0, arg1, arg2, arg3, arg4, arg5)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamBoardsInsights", reflect.TypeOf((*MockStore)(nil).GetTeamBoardsInsights), arg0, arg1, arg2, arg3, arg4)
}
// GetTeamCount mocks base method.
@ -1593,6 +1593,20 @@ func (mr *MockStoreMockRecorder) SendMessage(arg0, arg1, arg2 interface{}) *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockStore)(nil).SendMessage), arg0, arg1, arg2)
}
// SetBoardVisibility mocks base method.
func (m *MockStore) SetBoardVisibility(arg0, arg1, arg2 string, arg3 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetBoardVisibility", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error)
return ret0
}
// SetBoardVisibility indicates an expected call of SetBoardVisibility.
func (mr *MockStoreMockRecorder) SetBoardVisibility(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBoardVisibility", reflect.TypeOf((*MockStore)(nil).SetBoardVisibility), arg0, arg1, arg2, arg3)
}
// SetSystemSetting mocks base method.
func (m *MockStore) SetSystemSetting(arg0, arg1 string) error {
m.ctrl.T.Helper()

View File

@ -14,7 +14,7 @@ import (
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func (s *SQLStore) getTeamBoardsInsights(db sq.BaseRunner, teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) {
func (s *SQLStore) getTeamBoardsInsights(db sq.BaseRunner, teamID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) {
boardsHistoryQuery := s.getQueryBuilder(db).
Select("boards.id, boards.icon, boards.title, count(boards_history.id) as count, boards_history.modified_by, boards.created_by").
From(s.tablePrefix + "boards_history as boards_history").

View File

@ -19,14 +19,14 @@ func (s *SQLStore) getUserCategoryBoards(db sq.BaseRunner, userID, teamID string
userCategoryBoards := []model.CategoryBoards{}
for _, category := range categories {
boardIDs, err := s.getCategoryBoardAttributes(db, category.ID)
boardMetadata, err := s.getCategoryBoardAttributes(db, category.ID)
if err != nil {
return nil, err
}
userCategoryBoard := model.CategoryBoards{
Category: category,
BoardIDs: boardIDs,
Category: category,
BoardMetadata: boardMetadata,
}
userCategoryBoards = append(userCategoryBoards, userCategoryBoard)
@ -35,13 +35,12 @@ func (s *SQLStore) getUserCategoryBoards(db sq.BaseRunner, userID, teamID string
return userCategoryBoards, nil
}
func (s *SQLStore) getCategoryBoardAttributes(db sq.BaseRunner, categoryID string) ([]string, error) {
func (s *SQLStore) getCategoryBoardAttributes(db sq.BaseRunner, categoryID string) ([]model.CategoryBoardMetadata, error) {
query := s.getQueryBuilder(db).
Select("board_id").
Select("board_id, COALESCE(hidden, false)").
From(s.tablePrefix + "category_boards").
Where(sq.Eq{
"category_id": categoryID,
"delete_at": 0,
}).
OrderBy("sort_order")
@ -54,21 +53,17 @@ func (s *SQLStore) getCategoryBoardAttributes(db sq.BaseRunner, categoryID strin
return s.categoryBoardsFromRows(rows)
}
func (s *SQLStore) addUpdateCategoryBoard(db sq.BaseRunner, userID string, boardCategoryMapping map[string]string) error {
boardIDs := []string{}
for boardID := range boardCategoryMapping {
boardIDs = append(boardIDs, boardID)
}
func (s *SQLStore) addUpdateCategoryBoard(db sq.BaseRunner, userID, categoryID string, boardIDsParam []string) error {
// we need to de-duplicate this array as Postgres failes to
// handle upsert if there are multiple incoming rows
// that conflict the same existing row.
// For example, having the entry "1" in DB and trying to upsert "1" and "1" will fail
// as there are multiple duplicates of the same "1".
//
// Source: https://stackoverflow.com/questions/42994373/postgresql-on-conflict-cannot-affect-row-a-second-time
boardIDs := utils.DedupeStringArr(boardIDsParam)
if err := s.deleteUserCategoryBoards(db, userID, boardIDs); err != nil {
return err
}
return s.addUserCategoryBoard(db, userID, boardCategoryMapping)
}
func (s *SQLStore) addUserCategoryBoard(db sq.BaseRunner, userID string, boardCategoryMapping map[string]string) error {
if len(boardCategoryMapping) == 0 {
if len(boardIDs) == 0 {
return nil
}
@ -81,73 +76,62 @@ func (s *SQLStore) addUserCategoryBoard(db sq.BaseRunner, userID string, boardCa
"board_id",
"create_at",
"update_at",
"delete_at",
"sort_order",
"hidden",
)
now := utils.GetMillis()
for boardID, categoryID := range boardCategoryMapping {
query = query.
Values(
utils.NewID(utils.IDTypeNone),
userID,
categoryID,
boardID,
now,
now,
0,
0,
)
for _, boardID := range boardIDs {
query = query.Values(
utils.NewID(utils.IDTypeNone),
userID,
categoryID,
boardID,
now,
now,
0,
false,
)
}
if s.dbType == model.MysqlDBType {
query = query.Suffix(
"ON DUPLICATE KEY UPDATE category_id = ?",
categoryID,
)
} else {
query = query.Suffix(
`ON CONFLICT (user_id, board_id)
DO UPDATE SET category_id = EXCLUDED.category_id, update_at = EXCLUDED.update_at`,
)
}
if _, err := query.Exec(); err != nil {
s.logger.Error("addUserCategoryBoard error", mlog.Err(err))
return err
}
return nil
}
func (s *SQLStore) deleteUserCategoryBoards(db sq.BaseRunner, userID string, boardIDs []string) error {
if len(boardIDs) == 0 {
return nil
}
_, err := s.getQueryBuilder(db).
Update(s.tablePrefix+"category_boards").
Set("delete_at", utils.GetMillis()).
Where(sq.Eq{
"user_id": userID,
"board_id": boardIDs,
"delete_at": 0,
}).Exec()
if err != nil {
s.logger.Error(
"deleteUserCategoryBoards delete error",
mlog.String("userID", userID),
mlog.Array("boardID", boardIDs),
mlog.Err(err),
return fmt.Errorf(
"store addUpdateCategoryBoard: failed to upsert user-board-category userID: %s, categoryID: %s, board_count: %d, error: %w",
userID, categoryID, len(boardIDs), err,
)
return err
}
return nil
}
func (s *SQLStore) categoryBoardsFromRows(rows *sql.Rows) ([]string, error) {
blocks := []string{}
func (s *SQLStore) categoryBoardsFromRows(rows *sql.Rows) ([]model.CategoryBoardMetadata, error) {
metadata := []model.CategoryBoardMetadata{}
for rows.Next() {
boardID := ""
if err := rows.Scan(&boardID); err != nil {
datum := model.CategoryBoardMetadata{}
err := rows.Scan(&datum.BoardID, &datum.Hidden)
if err != nil {
s.logger.Error("categoryBoardsFromRows row scan error", mlog.Err(err))
return nil, err
}
blocks = append(blocks, boardID)
metadata = append(metadata, datum)
}
return blocks, nil
return metadata, nil
}
func (s *SQLStore) reorderCategoryBoards(db sq.BaseRunner, categoryID string, newBoardsOrder []string) ([]string, error) {
@ -166,7 +150,6 @@ func (s *SQLStore) reorderCategoryBoards(db sq.BaseRunner, categoryID string, ne
Set("sort_order", updateCase).
Where(sq.Eq{
"category_id": categoryID,
"delete_at": 0,
})
if _, err := query.Exec(); err != nil {
@ -181,3 +164,28 @@ func (s *SQLStore) reorderCategoryBoards(db sq.BaseRunner, categoryID string, ne
return newBoardsOrder, nil
}
func (s *SQLStore) setBoardVisibility(db sq.BaseRunner, userID, categoryID, boardID string, visible bool) error {
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"category_boards").
Set("hidden", !visible).
Where(sq.Eq{
"user_id": userID,
"category_id": categoryID,
"board_id": boardID,
})
if _, err := query.Exec(); err != nil {
s.logger.Error(
"SQLStore setBoardVisibility: failed to update board visibility",
mlog.String("user_id", userID),
mlog.String("board_id", boardID),
mlog.Bool("visible", visible),
mlog.Err(err),
)
return err
}
return nil
}

View File

@ -295,13 +295,14 @@ func (s *SQLStore) ensureMigrationsAppliedUpToVersion(engine *morph.Morph, drive
func (s *SQLStore) GetTemplateHelperFuncs() template.FuncMap {
funcs := template.FuncMap{
"addColumnIfNeeded": s.genAddColumnIfNeeded,
"dropColumnIfNeeded": s.genDropColumnIfNeeded,
"createIndexIfNeeded": s.genCreateIndexIfNeeded,
"renameTableIfNeeded": s.genRenameTableIfNeeded,
"renameColumnIfNeeded": s.genRenameColumnIfNeeded,
"doesTableExist": s.doesTableExist,
"doesColumnExist": s.doesColumnExist,
"addColumnIfNeeded": s.genAddColumnIfNeeded,
"dropColumnIfNeeded": s.genDropColumnIfNeeded,
"createIndexIfNeeded": s.genCreateIndexIfNeeded,
"renameTableIfNeeded": s.genRenameTableIfNeeded,
"renameColumnIfNeeded": s.genRenameColumnIfNeeded,
"doesTableExist": s.doesTableExist,
"doesColumnExist": s.doesColumnExist,
"addConstraintIfNeeded": s.genAddConstraintIfNeeded,
}
return funcs
}
@ -607,6 +608,67 @@ func (s *SQLStore) doesColumnExist(tableName, columnName string) (bool, error) {
return exists, nil
}
func (s *SQLStore) genAddConstraintIfNeeded(tableName, constraintName, constraintType, constraintDefinition string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
var query string
vars := map[string]string{
"schema": s.schemaName,
"constraint_name": constraintName,
"constraint_type": constraintType,
"table_name": tableName,
"constraint_definition": constraintDefinition,
"norm_table_name": normTableName,
}
switch s.dbType {
case model.SqliteDBType:
// SQLite doesn't have a generic way to add constraint. For example, you can only create indexes on existing tables.
// For other constraints, you need to re-build the table. So skipping here.
// Include SQLite specific migration in original migration file.
query = fmt.Sprintf("\n-- Sqlite3 cannot drop constraints; drop constraint '%s' in table '%s' skipped\n", constraintName, tableName)
case model.MysqlDBType:
query = replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE constraint_schema = '[[schema]]'
AND constraint_name = '[[constraint_name]]'
AND constraint_type = '[[constraint_type]]'
AND table_name = '[[table_name]]'
) > 0,
'SELECT 1;',
'ALTER TABLE [[norm_table_name]] ADD CONSTRAINT [[constraint_name]] [[constraint_definition]];'
));
PREPARE addConstraintIfNeeded FROM @stmt;
EXECUTE addConstraintIfNeeded;
DEALLOCATE PREPARE addConstraintIfNeeded;
`, vars)
case model.PostgresDBType:
query = replaceVars(`
DO
$$
BEGIN
IF NOT EXISTS (
SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE constraint_schema = '[[schema]]'
AND constraint_name = '[[constraint_name]]'
AND constraint_type = '[[constraint_type]]'
AND table_name = '[[table_name]]'
) THEN
ALTER TABLE [[norm_table_name]] ADD CONSTRAINT [[constraint_name]] [[constraint_definition]];
END IF;
END;
$$
LANGUAGE plpgsql;
`, vars)
}
return query, nil
}
func addPrefixIfNeeded(s, prefix string) string {
if !strings.HasPrefix(s, prefix) {
return prefix + s

View File

@ -0,0 +1 @@
DELETE FROM {{.prefix}}category_boards WHERE delete_at > 0;

View File

@ -0,0 +1,3 @@
{{ if or .postgres .mysql }}
{{ dropColumnIfNeeded "category_boards" "delete_at" }}
{{end}}

View File

@ -0,0 +1 @@
{{ addColumnIfNeeded "category_boards" "hidden" "boolean" "" }}

View File

@ -0,0 +1,26 @@
{{if or .mysql .postgres}}
{{ addConstraintIfNeeded "category_boards" "unique_user_category_board" "UNIQUE" "UNIQUE(user_id, board_id)"}}
{{end}}
{{if .sqlite}}
ALTER TABLE {{.prefix}}category_boards RENAME TO {{.prefix}}category_boards_old;
CREATE TABLE {{.prefix}}category_boards (
id varchar(36) NOT NULL,
user_id varchar(36) NOT NULL,
category_id varchar(36) NOT NULL,
board_id VARCHAR(36) NOT NULL,
create_at BIGINT,
update_at BIGINT,
sort_order BIGINT,
hidden boolean,
PRIMARY KEY (id),
CONSTRAINT unique_user_category_board UNIQUE (user_id, board_id)
);
INSERT INTO {{.prefix}}category_boards
(id, user_id, category_id, board_id, create_at, update_at, sort_order, hidden)
SELECT id, user_id, category_id, board_id, create_at, update_at, sort_order, hidden FROM {{.prefix}}category_boards_old;
DROP TABLE {{.prefix}}category_boards_old;
{{end}}

View File

@ -0,0 +1,57 @@
{{if .plugin}}
{{if .mysql}}
UPDATE {{.prefix}}category_boards AS fcb
JOIN Preferences p
ON fcb.user_id = p.userid
AND p.category = 'focalboard'
AND p.name = 'hiddenBoardIDs'
SET hidden = true
WHERE p.value LIKE concat('%', fcb.board_id, '%');
{{end}}
{{if .postgres}}
UPDATE {{.prefix}}category_boards as fcb
SET hidden = true
FROM preferences p
WHERE p.userid = fcb.user_id
AND p.category = 'focalboard'
AND p.name = 'hiddenBoardIDs'
AND p.value like ('%' || fcb.board_id || '%');
{{end}}
{{else}}
{{if .mysql}}
UPDATE {{.prefix}}category_boards AS fcb
JOIN {{.prefix}}preferences p
ON fcb.user_id = p.userid
AND p.category = 'focalboard'
AND p.name = 'hiddenBoardIDs'
SET hidden = true
WHERE p.value LIKE concat('%', fcb.board_id, '%');
{{end}}
{{if .postgres}}
UPDATE {{.prefix}}category_boards as fcb
SET hidden = true
FROM {{.prefix}}preferences p
WHERE p.userid = fcb.user_id
AND p.category = 'focalboard'
AND p.name = 'hiddenBoardIDs'
AND p.value like ('%' || fcb.board_id || '%');
{{end}}
{{end}}
{{if .sqlite}}
UPDATE {{.prefix}}category_boards
SET hidden = true
WHERE (user_id || '_' || board_id)
IN
(
SELECT (fcb.user_id || '_' || fcb.board_id)
FROM {{.prefix}}category_boards AS fcb
JOIN {{.prefix}}preferences p
ON p.userid = fcb.user_id
AND p.category = 'focalboard'
AND p.name = 'hiddenBoardIDs' WHERE
p.value LIKE ('%' || fcb.board_id || '%')
);
{{end}}

View File

@ -0,0 +1,5 @@
{{if .plugin}}
DELETE FROM Preferences WHERE category = 'focalboard' AND name = 'hiddenBoardIDs';
{{else}}
DELETE FROM {{.prefix}}preferences WHERE category = 'focalboard' AND name = 'hiddenBoardIDs';
{{end}}

View File

@ -0,0 +1,6 @@
INSERT INTO focalboard_category_boards 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);

View File

@ -0,0 +1,6 @@
INSERT INTO focalboard_category_boards 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);

View File

@ -0,0 +1 @@
ALTER TABLE focalboard_category_boards DROP COLUMN delete_at;

View File

@ -0,0 +1 @@
ALTER TABLE focalboard_category_boards ADD COLUMN hidden boolean;

View File

@ -0,0 +1,10 @@
INSERT INTO focalboard_category_boards 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);
INSERT INTO Preferences VALUES
('user-id-1', 'focalboard', 'hiddenBoardIDs', '["board-id-1"]'),
('user-id-2', 'focalboard', 'hiddenBoardIDs', '["board-id-3", "board-id-4"]');

View File

@ -0,0 +1,6 @@
INSERT INTO focalboard_category_boards 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);

View File

@ -0,0 +1,10 @@
INSERT INTO focalboard_category_boards 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);
INSERT INTO Preferences VALUES
('user-id-1', 'focalboard', 'hiddenBoardIDs', ''),
('user-id-2', 'focalboard', 'hiddenBoardIDs', '');

View File

@ -0,0 +1,10 @@
INSERT INTO focalboard_category_boards 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);
INSERT INTO focalboard_preferences VALUES
('user-id-1', 'focalboard', 'hiddenBoardIDs', '["board-id-1"]'),
('user-id-2', 'focalboard', 'hiddenBoardIDs', '["board-id-3", "board-id-4"]');

View File

@ -0,0 +1,10 @@
INSERT INTO focalboard_category_boards 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);
INSERT INTO focalboard_preferences VALUES
('user-id-1', 'focalboard', 'hiddenBoardIDs', ''),
('user-id-2', 'focalboard', 'hiddenBoardIDs', '');

View File

@ -0,0 +1,5 @@
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-3', 'lorem', 'lorem', ''),
('user-id-4', 'ipsum', 'ipsum', '');

View File

@ -0,0 +1,5 @@
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', 'lorem', 'lorem', ''),
('user-id-2', 'ipsum', 'ipsum', '');

View File

@ -15,6 +15,18 @@ type TestHelper struct {
isPlugin bool
}
func (th *TestHelper) IsPostgres() bool {
return th.f.DB().DriverName() == "postgres"
}
func (th *TestHelper) IsMySQL() bool {
return th.f.DB().DriverName() == "mysql"
}
func (th *TestHelper) IsSQLite() bool {
return th.f.DB().DriverName() == "sqlite3"
}
func SetupPluginTestHelper(t *testing.T) (*TestHelper, func()) {
dbType := strings.TrimSpace(os.Getenv("FOCALBOARD_STORE_TEST_DB_TYPE"))
if dbType == "" || dbType == model.SqliteDBType {

View File

@ -0,0 +1,57 @@
package migrationstests
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test34DropDeleteAtColumnMySQLPostgres(t *testing.T) {
t.Run("column exists", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.f.MigrateToStep(34)
// migration 34 only works for MySQL and PostgreSQL
if th.IsMySQL() {
var count int
query := "SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'focalboard_category_boards' AND column_name = 'delete_at'"
th.f.DB().Get(&count, query)
require.Equal(t, 0, count)
} else if th.IsPostgres() {
var count int
query := "select count(*) from information_schema.columns where table_name = 'focalboard_category_boards' and column_name = 'delete_at'"
th.f.DB().Get(&count, query)
require.Equal(t, 0, count)
}
})
t.Run("column already deleted", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
// For migration 34, we don't drop column
// on SQLite, so no need to test for it.
if th.IsSQLite() {
return
}
th.f.MigrateToStep(33).
ExecFile("./fixtures/test34_drop_delete_at_column.sql")
th.f.MigrateToStep(34)
if th.IsMySQL() {
var count int
query := "SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'focalboard_category_boards' AND column_name = 'delete_at'"
th.f.DB().Get(&count, query)
require.Equal(t, 0, count)
} else if th.IsPostgres() {
var count int
query := "select count(*) from information_schema.columns where table_name = 'focalboard_category_boards' and column_name = 'delete_at'"
th.f.DB().Get(&count, query)
require.Equal(t, 0, count)
}
})
}

View File

@ -0,0 +1,27 @@
package migrationstests
import "testing"
func Test35AddHIddenColumnToCategoryBoards(t *testing.T) {
t.Run("base case - column doesn't already exist", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.f.MigrateToStep(35)
})
t.Run("column already exist", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
// We don't support adding column in idempotent manner
// for SQLite, so no need to check for it.
if th.IsSQLite() {
return
}
th.f.MigrateToStep(34).
ExecFile("./fixtures/test35_add_hidden_column.sql")
th.f.MigrateToStep(35)
})
}

View File

@ -0,0 +1,69 @@
package migrationstests
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test36AddUniqueConstraintToCategoryBoards(t *testing.T) {
t.Run("constraint doesn't alreadt exists", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.f.MigrateToStep(36)
// verifying if constraint has been added
//can't verify in sqlite, so skipping it
if th.IsSQLite() {
return
}
var count int
query := "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " +
"WHERE constraint_name = 'unique_user_category_board' " +
"AND constraint_type = 'UNIQUE' " +
"AND table_name = 'focalboard_category_boards'"
th.f.DB().Get(&count, query)
require.Equal(t, 1, count)
})
t.Run("constraint already exists", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
// SQLIte doesn't support adding constraint to existing table
// and neither do we, so skipping for sqlite
if th.IsSQLite() {
return
}
th.f.MigrateToStep(35)
if th.IsMySQL() {
th.f.DB().Exec("alter table focalboard_category_boards add constraint unique_user_category_board UNIQUE(user_id, board_id);")
} else if th.IsPostgres() {
th.f.DB().Exec("ALTER TABLE focalboard_category_boards ADD CONSTRAINT unique_user_category_board UNIQUE(user_id, board_id);")
}
th.f.MigrateToStep(36)
var schema string
if th.IsMySQL() {
schema = "DATABASE()"
} else if th.IsPostgres() {
schema = "'public'"
}
var count int
query := "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " +
"WHERE constraint_schema = " + schema + " " +
"AND constraint_name = 'unique_user_category_board' " +
"AND constraint_type = 'UNIQUE' " +
"AND table_name = 'focalboard_category_boards'"
th.f.DB().Get(&count, query)
require.Equal(t, 1, count)
})
}

View File

@ -0,0 +1,134 @@
package migrationstests
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test37MigrateHiddenBoardIDTest(t *testing.T) {
t.Run("no existing hidden boards exist", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.f.MigrateToStep(37)
})
t.Run("SQLite - existsing category boards with some hidden boards", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
if th.IsMySQL() || th.IsPostgres() {
return
}
th.f.MigrateToStep(36).
ExecFile("./fixtures/test37_valid_data_sqlite.sql")
th.f.MigrateToStep(37)
type categoryBoard struct {
User_ID string
Category_ID string
Board_ID string
Hidden bool
}
var hiddenCategoryBoards []categoryBoard
query := "SELECT user_id, category_id, board_id, hidden FROM focalboard_category_boards WHERE hidden = true"
err := th.f.DB().Select(&hiddenCategoryBoards, query)
require.NoError(t, err)
require.Equal(t, 3, len(hiddenCategoryBoards))
require.Contains(t, hiddenCategoryBoards, categoryBoard{User_ID: "user-id-1", Category_ID: "category-id-1", Board_ID: "board-id-1", Hidden: true})
require.Contains(t, hiddenCategoryBoards, categoryBoard{User_ID: "user-id-2", Category_ID: "category-id-3", Board_ID: "board-id-3", Hidden: true})
require.Contains(t, hiddenCategoryBoards, categoryBoard{User_ID: "user-id-2", Category_ID: "category-id-3", Board_ID: "board-id-4", Hidden: true})
})
t.Run("MySQL and PostgreSQL - existsing category boards with some hidden boards", func(t *testing.T) {
th, tearDown := SetupPluginTestHelper(t)
defer tearDown()
if th.IsSQLite() {
return
}
th.f.MigrateToStep(36).
ExecFile("./fixtures/test37_valid_data.sql")
th.f.MigrateToStep(37)
type categoryBoard struct {
User_ID string
Category_ID string
Board_ID string
Hidden bool
}
var hiddenCategoryBoards []categoryBoard
query := "SELECT user_id, category_id, board_id, hidden FROM focalboard_category_boards WHERE hidden = true"
err := th.f.DB().Select(&hiddenCategoryBoards, query)
require.NoError(t, err)
require.Equal(t, 3, len(hiddenCategoryBoards))
require.Contains(t, hiddenCategoryBoards, categoryBoard{User_ID: "user-id-1", Category_ID: "category-id-1", Board_ID: "board-id-1", Hidden: true})
require.Contains(t, hiddenCategoryBoards, categoryBoard{User_ID: "user-id-2", Category_ID: "category-id-3", Board_ID: "board-id-3", Hidden: true})
require.Contains(t, hiddenCategoryBoards, categoryBoard{User_ID: "user-id-2", Category_ID: "category-id-3", Board_ID: "board-id-4", Hidden: true})
})
t.Run("no hidden boards", func(t *testing.T) {
th, tearDown := SetupPluginTestHelper(t)
defer tearDown()
th.f.MigrateToStep(36).
ExecFile("./fixtures/test37_valid_data_no_hidden_boards.sql")
th.f.MigrateToStep(37)
var count int
query := "SELECT count(*) FROM focalboard_category_boards WHERE hidden = true"
err := th.f.DB().Get(&count, query)
require.NoError(t, err)
require.Equal(t, 0, count)
})
t.Run("SQLite - preference but no hidden board", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
if th.IsMySQL() || th.IsPostgres() {
return
}
th.f.MigrateToStep(36).
ExecFile("./fixtures/test37_valid_data_sqlite_preference_but_no_hidden_board.sql")
th.f.MigrateToStep(37)
var count int
query := "SELECT count(*) FROM focalboard_category_boards WHERE hidden = true"
err := th.f.DB().Get(&count, query)
require.NoError(t, err)
require.Equal(t, 0, count)
})
t.Run("MySQL and PostgreSQL - preference but no hidden board", func(t *testing.T) {
th, tearDown := SetupPluginTestHelper(t)
defer tearDown()
if th.IsSQLite() {
return
}
th.f.MigrateToStep(36).
ExecFile("./fixtures/test37_valid_data_preference_but_no_hidden_board.sql")
th.f.MigrateToStep(37)
var count int
query := "SELECT count(*) FROM focalboard_category_boards WHERE hidden = true"
err := th.f.DB().Get(&count, query)
require.NoError(t, err)
require.Equal(t, 0, count)
})
}

View File

@ -0,0 +1,63 @@
package migrationstests
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test38RemoveHiddenBoardIDsFromPreferences(t *testing.T) {
t.Run("standalone - no data exist", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.f.MigrateToStep(38)
})
t.Run("plugin - no data exist", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.f.MigrateToStep(38)
})
t.Run("standalone - some data exist", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.f.MigrateToStep(37).
ExecFile("./fixtures/test38_add_standalone_preferences.sql")
// verify existing data count
var count int
countQuery := "SELECT COUNT(*) FROM focalboard_preferences"
err := th.f.DB().Get(&count, countQuery)
require.NoError(t, err)
require.Equal(t, 4, count)
th.f.MigrateToStep(38)
// now the count should be 0
err = th.f.DB().Get(&count, countQuery)
require.NoError(t, err)
require.Equal(t, 2, count)
})
t.Run("plugin - some data exist", func(t *testing.T) {
th, tearDown := SetupPluginTestHelper(t)
defer tearDown()
th.f.MigrateToStep(37).
ExecFile("./fixtures/test38_add_plugin_preferences.sql")
// verify existing data count
var count int
countQuery := "SELECT COUNT(*) FROM Preferences"
err := th.f.DB().Get(&count, countQuery)
require.NoError(t, err)
require.Equal(t, 4, count)
th.f.MigrateToStep(38)
// now the count should be 0
err = th.f.DB().Get(&count, countQuery)
require.NoError(t, err)
require.Equal(t, 2, count)
})
}

View File

@ -0,0 +1,63 @@
package migrationstests
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test33RemoveDeletedCategoryBoards(t *testing.T) {
t.Run("base case - no data in table", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.f.MigrateToStep(33)
})
t.Run("existing data - 2 soft deleted records", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.f.MigrateToStep(32).
ExecFile("./fixtures/test33_with_deleted_data.sql")
// cound total records
var count int
err := th.f.DB().Get(&count, "SELECT COUNT(*) FROM focalboard_category_boards")
require.NoError(t, err)
require.Equal(t, 5, count)
// now we run the migration
th.f.MigrateToStep(33)
// and verify record count again.
// The soft deleted records should have been removed from the DB now
err = th.f.DB().Get(&count, "SELECT COUNT(*) FROM focalboard_category_boards")
require.NoError(t, err)
require.Equal(t, 3, count)
})
t.Run("existing data - no soft deleted records", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.f.MigrateToStep(32).
ExecFile("./fixtures/test33_with_no_deleted_data.sql")
// cound total records
var count int
err := th.f.DB().Get(&count, "SELECT COUNT(*) FROM focalboard_category_boards")
require.NoError(t, err)
require.Equal(t, 5, count)
// now we run the migration
th.f.MigrateToStep(33)
// and verify record count again.
// Since there were no soft-deleted records, nothing should have been
// deleted from the database.
err = th.f.DB().Get(&count, "SELECT COUNT(*) FROM focalboard_category_boards")
require.NoError(t, err)
require.Equal(t, 5, count)
})
}

View File

@ -22,15 +22,15 @@ import (
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func (s *SQLStore) AddUpdateCategoryBoard(userID string, boardCategoryMapping map[string]string) error {
func (s *SQLStore) AddUpdateCategoryBoard(userID string, categoryID string, boardIDs []string) error {
if s.dbType == model.SqliteDBType {
return s.addUpdateCategoryBoard(s.db, userID, boardCategoryMapping)
return s.addUpdateCategoryBoard(s.db, userID, categoryID, boardIDs)
}
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.addUpdateCategoryBoard(tx, userID, boardCategoryMapping)
err := s.addUpdateCategoryBoard(tx, userID, categoryID, boardIDs)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "AddUpdateCategoryBoard"))
@ -144,7 +144,7 @@ func (s *SQLStore) CreateUser(user *model.User) (*model.User, error) {
}
func (s *SQLStore) DBVersion() string {
return s.dBVersion(s.db)
return s.dBVersion()
}
@ -528,8 +528,8 @@ func (s *SQLStore) GetTeam(ID string) (*model.Team, error) {
}
func (s *SQLStore) GetTeamBoardsInsights(teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) {
return s.getTeamBoardsInsights(s.db, teamID, userID, since, offset, limit, boardIDs)
func (s *SQLStore) GetTeamBoardsInsights(teamID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) {
return s.getTeamBoardsInsights(s.db, teamID, since, offset, limit, boardIDs)
}
@ -865,6 +865,11 @@ func (s *SQLStore) SendMessage(message string, postType string, receipts []strin
}
func (s *SQLStore) SetBoardVisibility(userID string, categoryID string, boardID string, visible bool) error {
return s.setBoardVisibility(s.db, userID, categoryID, boardID, visible)
}
func (s *SQLStore) SetSystemSetting(key string, value string) error {
return s.setSystemSetting(s.db, key, value)

View File

@ -136,7 +136,7 @@ func (s *SQLStore) getQueryBuilder(db sq.BaseRunner) sq.StatementBuilderType {
return builder.RunWith(db)
}
func (s *SQLStore) escapeField(fieldName string) string {
func (s *SQLStore) escapeField(fieldName string) string { //nolint:unparam
if s.dbType == model.MysqlDBType {
return "`" + fieldName + "`"
}
@ -185,7 +185,7 @@ func (s *SQLStore) getChannel(db sq.BaseRunner, teamID, channel string) (*mmMode
return nil, store.NewNotSupportedError("get channel not supported on standalone mode")
}
func (s *SQLStore) dBVersion(db sq.BaseRunner) string {
func (s *SQLStore) dBVersion() string {
var version string
var row *sql.Row

View File

@ -131,8 +131,9 @@ type Store interface {
SaveFileInfo(fileInfo *mmModel.FileInfo) error
// @withTransaction
AddUpdateCategoryBoard(userID string, boardCategoryMapping map[string]string) error
AddUpdateCategoryBoard(userID, categoryID string, boardIDs []string) error
ReorderCategoryBoards(categoryID string, newBoardsOrder []string) ([]string, error)
SetBoardVisibility(userID, categoryID, boardID string, visible bool) error
CreateSubscription(sub *model.Subscription) (*model.Subscription, error)
DeleteSubscription(blockID string, subscriberID string) error
@ -168,7 +169,7 @@ type Store interface {
SendMessage(message, postType string, receipts []string) error
// Insights
GetTeamBoardsInsights(teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error)
GetTeamBoardsInsights(teamID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error)
GetUserBoardsInsights(teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error)
GetUserTimezone(userID string) (string, error)

View File

@ -72,7 +72,7 @@ func getBoardsInsightsTest(t *testing.T, store store.Store) {
boardsUser2, _ := store.GetBoardsForUserAndTeam(testInsightsUserID1, testTeamID, true)
t.Run("team insights", func(t *testing.T) {
boardIDs := []string{boardsUser1[0].ID, boardsUser1[1].ID, boardsUser1[2].ID}
topTeamBoards, err := store.GetTeamBoardsInsights(testTeamID, testUserID,
topTeamBoards, err := store.GetTeamBoardsInsights(testTeamID,
0, 0, 10, boardIDs)
require.NoError(t, err)
require.Len(t, topTeamBoards.Items, 3)

View File

@ -294,11 +294,11 @@ func testReorderCategoryBoards(t *testing.T, store store.Store) {
})
assert.NoError(t, err)
err = store.AddUpdateCategoryBoard("user_id", map[string]string{
"board_id_1": "category_id_1",
"board_id_2": "category_id_1",
"board_id_3": "category_id_1",
"board_id_4": "category_id_1",
err = store.AddUpdateCategoryBoard("user_id", "category_id_1", []string{
"board_id_1",
"board_id_2",
"board_id_3",
"board_id_4",
})
assert.NoError(t, err)
@ -306,11 +306,11 @@ func testReorderCategoryBoards(t *testing.T, store store.Store) {
categoryBoards, err := store.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, 4, len(categoryBoards[0].BoardIDs))
assert.Contains(t, categoryBoards[0].BoardIDs, "board_id_1")
assert.Contains(t, categoryBoards[0].BoardIDs, "board_id_2")
assert.Contains(t, categoryBoards[0].BoardIDs, "board_id_3")
assert.Contains(t, categoryBoards[0].BoardIDs, "board_id_4")
assert.Equal(t, 4, len(categoryBoards[0].BoardMetadata))
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_1", Hidden: false})
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_2", Hidden: false})
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_3", Hidden: false})
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_4", Hidden: false})
// reordering
newOrder, err := store.ReorderCategoryBoards("category_id_1", []string{
@ -329,9 +329,9 @@ func testReorderCategoryBoards(t *testing.T, store store.Store) {
categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, 4, len(categoryBoards[0].BoardIDs))
assert.Equal(t, "board_id_3", categoryBoards[0].BoardIDs[0])
assert.Equal(t, "board_id_1", categoryBoards[0].BoardIDs[1])
assert.Equal(t, "board_id_2", categoryBoards[0].BoardIDs[2])
assert.Equal(t, "board_id_4", categoryBoards[0].BoardIDs[3])
assert.Equal(t, 4, len(categoryBoards[0].BoardMetadata))
assert.Equal(t, "board_id_3", categoryBoards[0].BoardMetadata[0].BoardID)
assert.Equal(t, "board_id_1", categoryBoards[0].BoardMetadata[1].BoardID)
assert.Equal(t, "board_id_2", categoryBoards[0].BoardMetadata[2].BoardID)
assert.Equal(t, "board_id_4", categoryBoards[0].BoardMetadata[3].BoardID)
}

View File

@ -15,6 +15,18 @@ func StoreTestCategoryBoardsStore(t *testing.T, setup func(t *testing.T) (store.
defer tearDown()
testGetUserCategoryBoards(t, store)
})
t.Run("AddUpdateCategoryBoard", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testAddUpdateCategoryBoard(t, store)
})
t.Run("SetBoardVisibility", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testSetBoardVisibility(t, store)
})
}
func testGetUserCategoryBoards(t *testing.T, store store.Store) {
@ -60,14 +72,14 @@ func testGetUserCategoryBoards(t *testing.T, store store.Store) {
// Adding Board 1 and Board 2 to Category 1
// The boards don't need to exists in DB for this test
err = store.AddUpdateCategoryBoard("user_id_1", map[string]string{"board_1": "category_id_1"})
err = store.AddUpdateCategoryBoard("user_id_1", "category_id_1", []string{"board_1"})
assert.NoError(t, err)
err = store.AddUpdateCategoryBoard("user_id_1", map[string]string{"board_2": "category_id_1"})
err = store.AddUpdateCategoryBoard("user_id_1", "category_id_1", []string{"board_2"})
assert.NoError(t, err)
// Adding Board 3 to Category 2
err = store.AddUpdateCategoryBoard("user_id_1", map[string]string{"board_3": "category_id_2"})
err = store.AddUpdateCategoryBoard("user_id_1", "category_id_2", []string{"board_3"})
assert.NoError(t, err)
// we'll leave category 3 empty
@ -94,13 +106,13 @@ func testGetUserCategoryBoards(t *testing.T, store store.Store) {
}
assert.NotEmpty(t, category1BoardCategory)
assert.Equal(t, 2, len(category1BoardCategory.BoardIDs))
assert.Equal(t, 2, len(category1BoardCategory.BoardMetadata))
assert.NotEmpty(t, category1BoardCategory)
assert.Equal(t, 1, len(category2BoardCategory.BoardIDs))
assert.Equal(t, 1, len(category2BoardCategory.BoardMetadata))
assert.NotEmpty(t, category1BoardCategory)
assert.Equal(t, 0, len(category3BoardCategory.BoardIDs))
assert.Equal(t, 0, len(category3BoardCategory.BoardMetadata))
t.Run("get empty category boards", func(t *testing.T) {
userCategoryBoards, err := store.GetUserCategoryBoards("nonexistent-user-id", "nonexistent-team-id")
@ -108,3 +120,142 @@ func testGetUserCategoryBoards(t *testing.T, store store.Store) {
assert.Empty(t, userCategoryBoards)
})
}
func testAddUpdateCategoryBoard(t *testing.T, store store.Store) {
// creating few boards and categories to later associoate with the category
_, _, err := store.CreateBoardsAndBlocksWithAdmin(&model.BoardsAndBlocks{
Boards: []*model.Board{
{
ID: "board_id_1",
TeamID: "team_id",
},
{
ID: "board_id_2",
TeamID: "team_id",
},
},
}, "user_id")
assert.NoError(t, err)
err = store.CreateCategory(model.Category{
ID: "category_id",
Name: "Category",
UserID: "user_id",
TeamID: "team_id",
})
assert.NoError(t, err)
// adding a few boards to the category
err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_1", "board_id_2"})
assert.NoError(t, err)
// verify inserted data
categoryBoards, err := store.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, "category_id", categoryBoards[0].ID)
assert.Equal(t, 2, len(categoryBoards[0].BoardMetadata))
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_1", Hidden: false})
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_2", Hidden: false})
// adding new boards to the same category
err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_3"})
assert.NoError(t, err)
// verify inserted data
categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, "category_id", categoryBoards[0].ID)
assert.Equal(t, 3, len(categoryBoards[0].BoardMetadata))
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_3", Hidden: false})
// passing empty array
err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{})
assert.NoError(t, err)
// verify inserted data
categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, "category_id", categoryBoards[0].ID)
assert.Equal(t, 3, len(categoryBoards[0].BoardMetadata))
// passing duplicate data in input
err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_4", "board_id_4"})
assert.NoError(t, err)
// verify inserted data
categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, "category_id", categoryBoards[0].ID)
assert.Equal(t, 4, len(categoryBoards[0].BoardMetadata))
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_4", Hidden: false})
// adding already added board
err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_1", "board_id_2"})
assert.NoError(t, err)
// verify inserted data
categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, "category_id", categoryBoards[0].ID)
assert.Equal(t, 4, len(categoryBoards[0].BoardMetadata))
// passing already added board along with a new board
err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_1", "board_id_5"})
assert.NoError(t, err)
// verify inserted data
categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, "category_id", categoryBoards[0].ID)
assert.Equal(t, 5, len(categoryBoards[0].BoardMetadata))
assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_5", Hidden: false})
}
func testSetBoardVisibility(t *testing.T, store store.Store) {
_, _, err := store.CreateBoardsAndBlocksWithAdmin(&model.BoardsAndBlocks{
Boards: []*model.Board{
{
ID: "board_id_1",
TeamID: "team_id",
},
},
}, "user_id")
assert.NoError(t, err)
err = store.CreateCategory(model.Category{
ID: "category_id",
Name: "Category",
UserID: "user_id",
TeamID: "team_id",
})
assert.NoError(t, err)
// adding a few boards to the category
err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_1"})
assert.NoError(t, err)
err = store.SetBoardVisibility("user_id", "category_id", "board_id_1", true)
assert.NoError(t, err)
// verify set visibility
categoryBoards, err := store.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.Equal(t, 1, len(categoryBoards))
assert.Equal(t, "category_id", categoryBoards[0].ID)
assert.Equal(t, 1, len(categoryBoards[0].BoardMetadata))
assert.False(t, categoryBoards[0].BoardMetadata[0].Hidden)
err = store.SetBoardVisibility("user_id", "category_id", "board_id_1", false)
assert.NoError(t, err)
// verify set visibility
categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id")
assert.NoError(t, err)
assert.True(t, categoryBoards[0].BoardMetadata[0].Hidden)
}

View File

@ -92,7 +92,7 @@ func LoadData(t *testing.T, store store.Store) {
err = store.UpsertSharing(sharing)
require.NoError(t, err)
err = store.AddUpdateCategoryBoard(testUserID, map[string]string{boardID: categoryID})
err = store.AddUpdateCategoryBoard(testUserID, categoryID, []string{boardID})
require.NoError(t, err)
}

View File

@ -102,3 +102,20 @@ func IsCloudLicense(license *mmModel.License) bool {
license.Features.Cloud != nil &&
*license.Features.Cloud
}
func DedupeStringArr(arr []string) []string {
hashMap := map[string]bool{}
for _, item := range arr {
hashMap[item] = true
}
dedupedArr := make([]string, len(hashMap))
i := 0
for key := range hashMap {
dedupedArr[i] = key
i++
}
return dedupedArr
}

View File

@ -59,7 +59,8 @@ func NewServer(rootPath string, serverRoot string, port int, ssl, localOnly bool
baseURL = url.Path
ws := &Server{
Server: http.Server{
// (TODO: Add ReadHeaderTimeout)
Server: http.Server{ //nolint:gosec
Addr: addr,
Handler: r,
},

View File

@ -422,7 +422,7 @@ func (pa *PluginAdapter) sendTeamMessage(event, teamID string, payload map[strin
EnsureUsers: ensureUserIDs,
}
pa.sendMessageToCluster("websocket_message", clusterMessage)
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendTeamMessageSkipCluster(event, teamID, payload)
@ -447,7 +447,7 @@ func (pa *PluginAdapter) sendBoardMessage(teamID, boardID string, payload map[st
EnsureUsers: ensureUserIDs,
}
pa.sendMessageToCluster("websocket_message", clusterMessage)
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendBoardMessageSkipCluster(teamID, boardID, payload, ensureUserIDs...)
@ -490,7 +490,7 @@ func (pa *PluginAdapter) BroadcastCategoryChange(category model.Category) {
UserID: category.UserID,
}
pa.sendMessageToCluster("websocket_message", clusterMessage)
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendUserMessageSkipCluster(websocketActionUpdateCategory, payload, category.UserID)
@ -514,7 +514,7 @@ func (pa *PluginAdapter) BroadcastCategoryReorder(teamID, userID string, categor
UserID: userID,
}
pa.sendMessageToCluster("websocket_message", clusterMessage)
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendUserMessageSkipCluster(message.Action, payload, userID)
@ -540,7 +540,7 @@ func (pa *PluginAdapter) BroadcastCategoryBoardsReorder(teamID, userID, category
UserID: userID,
}
pa.sendMessageToCluster("websocket_message", clusterMessage)
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendUserMessageSkipCluster(message.Action, payload, userID)
@ -568,7 +568,7 @@ func (pa *PluginAdapter) BroadcastCategoryBoardChange(teamID, userID string, boa
UserID: userID,
}
pa.sendMessageToCluster("websocket_message", clusterMessage)
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendUserMessageSkipCluster(websocketActionUpdateCategoryBoard, utils.StructToMap(message), userID)

View File

@ -15,7 +15,8 @@ type ClusterMessage struct {
EnsureUsers []string
}
func (pa *PluginAdapter) sendMessageToCluster(id string, clusterMessage *ClusterMessage) {
func (pa *PluginAdapter) sendMessageToCluster(clusterMessage *ClusterMessage) {
const id = "websocket_message"
b, err := json.Marshal(clusterMessage)
if err != nil {
pa.logger.Error("couldn't get JSON bytes from cluster message",

View File

@ -120,7 +120,7 @@ const me: IUser = {
const categoryAttribute1 = TestBlockFactory.createCategoryBoards()
categoryAttribute1.name = 'Category 1'
categoryAttribute1.boardIDs = [board.id]
categoryAttribute1.boardMetadata = [{boardID: board.id, hidden: false}]
describe('src/components/shareBoard/shareBoard', () => {
const w = (window as any)

View File

@ -41,11 +41,12 @@ describe('components/sidebarSidebar', () => {
const categoryAttribute1 = TestBlockFactory.createCategoryBoards()
categoryAttribute1.id = 'category1'
categoryAttribute1.name = 'Category 1'
categoryAttribute1.boardIDs = [board.id]
categoryAttribute1.boardMetadata = [{boardID: board.id, hidden: false}]
const defaultCategory = TestBlockFactory.createCategoryBoards()
defaultCategory.id = 'default_category'
defaultCategory.name = 'Boards'
defaultCategory.boardMetadata = []
test('sidebar hidden', () => {
const store = mockStore({
@ -80,6 +81,7 @@ describe('components/sidebarSidebar', () => {
categoryAttributes: [
categoryAttribute1,
],
hiddenBoardIDs: [],
},
})
@ -111,6 +113,11 @@ describe('components/sidebarSidebar', () => {
customGlobal.innerWidth = 500
const localCategoryAttribute = TestBlockFactory.createCategoryBoards()
localCategoryAttribute.id = 'category1'
localCategoryAttribute.name = 'Category 1'
categoryAttribute1.boardMetadata = [{boardID: board.id, hidden: false}]
const store = mockStore({
teams: {
current: {id: 'team-id'},
@ -143,6 +150,7 @@ describe('components/sidebarSidebar', () => {
categoryAttributes: [
categoryAttribute1,
],
hiddenBoardIDs: [],
},
})
@ -169,6 +177,11 @@ describe('components/sidebarSidebar', () => {
})
test('dont show hidden boards', () => {
const localCategoryAttribute = TestBlockFactory.createCategoryBoards()
localCategoryAttribute.id = 'category1'
localCategoryAttribute.name = 'Category 1'
localCategoryAttribute.boardMetadata = [{boardID: board.id, hidden: true}]
const store = mockStore({
teams: {
current: {id: 'team-id'},
@ -203,8 +216,9 @@ describe('components/sidebarSidebar', () => {
},
sidebar: {
categoryAttributes: [
categoryAttribute1,
localCategoryAttribute,
],
hiddenBoardIDs: [board.id],
},
})
@ -236,6 +250,7 @@ describe('components/sidebarSidebar', () => {
collapsedCategory.id = 'categoryCollapsed'
collapsedCategory.name = 'Category 2'
collapsedCategory.collapsed = true
collapsedCategory.boardMetadata = []
const store = mockStore({
teams: {
@ -270,6 +285,7 @@ describe('components/sidebarSidebar', () => {
categoryAttribute1,
collapsedCategory,
],
hiddenBoardIDs: [],
},
})
@ -327,6 +343,7 @@ describe('components/sidebarSidebar', () => {
categoryAttribute1,
defaultCategory,
],
hiddenBoardIDs: [],
},
})
@ -355,7 +372,7 @@ describe('components/sidebarSidebar', () => {
const categoryAttribute2 = TestBlockFactory.createCategoryBoards()
categoryAttribute2.id = 'category2'
categoryAttribute2.name = 'Category 2'
categoryAttribute2.boardIDs = [board2.id]
categoryAttribute2.boardMetadata = [{boardID: board2.id, hidden: false}]
const store = mockStore({
teams: {

View File

@ -124,8 +124,8 @@ const Sidebar = (props: Props) => {
// and thats the first time that user is opening that board.
// Here we check if that board has a associated category for the user. If not,
// we assign it to the default "Boards" category.
// We do this on the client side rather than the server side live for all other cases
// is because there is no good, explicit API call to add this logic to when opening
// We do this on the client side rather than the server side like for all other cases
// because there is no good, explicit API call to add this logic to when opening
// a board that you have implicit access to.
useEffect(() => {
if (!sidebarCategories || sidebarCategories.length === 0 || !currentBoard || !team || currentBoard.isTemplate) {
@ -133,7 +133,8 @@ const Sidebar = (props: Props) => {
}
// find the category the current board belongs to
const category = sidebarCategories.find((c) => c.boardIDs.indexOf(currentBoard.id) >= 0)
// const category = sidebarCategories.find((c) => c.boardIDs.indexOf(currentBoard.id) >= 0)
const category = sidebarCategories.find((c) => c.boardMetadata.find((boardMetadata) => boardMetadata.boardID === currentBoard.id))
if (category) {
// Boards does belong to a category.
// All good here. Nothing to do
@ -219,29 +220,41 @@ const Sidebar = (props: Props) => {
return
}
const boardIDs = [...toSidebarCategory.boardIDs]
boardIDs.splice(source.index, 1)
boardIDs.splice(destination.index, 0, toSidebarCategory.boardIDs[source.index])
const categoryBoardMetadata = [...toSidebarCategory.boardMetadata]
categoryBoardMetadata.splice(source.index, 1)
categoryBoardMetadata.splice(destination.index, 0, toSidebarCategory.boardMetadata[source.index])
dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardIDs}))
await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, boardIDs)
dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: categoryBoardMetadata}))
const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID)
await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs)
} else {
// board moved to a different category
const toSidebarCategory = sidebarCategories.find((category) => category.id === toCategoryID)
const fromSidebarCategory = sidebarCategories.find((category) => category.id === fromCategoryID)
if (!toSidebarCategory) {
Utils.logError(`toCategoryID not found in list of sidebar categories. toCategoryID: ${toCategoryID}`)
return
}
const boardIDs = [...toSidebarCategory.boardIDs]
boardIDs.splice(destination.index, 0, boardID)
if (!fromSidebarCategory) {
Utils.logError(`fromCategoryID not found in list of sidebar categories. fromCategoryID: ${fromCategoryID}`)
return
}
const categoryBoardMetadata = [...toSidebarCategory.boardMetadata]
const fromCategoryBoardMetadata = fromSidebarCategory.boardMetadata[source.index]
categoryBoardMetadata.splice(destination.index, 0, fromCategoryBoardMetadata)
// optimistically updating the store to create a lag-free UI.
await dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardIDs}))
dispatch(updateBoardCategories([{boardID, categoryID: toCategoryID}]))
await dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: categoryBoardMetadata}))
dispatch(updateBoardCategories([{...fromCategoryBoardMetadata, categoryID: toCategoryID}]))
await mutator.moveBoardToCategory(team.id, boardID, toCategoryID, fromCategoryID)
await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, boardIDs)
const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID)
await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs)
}
}, [team, sidebarCategories])
@ -313,7 +326,7 @@ const Sidebar = (props: Props) => {
const getSortedCategoryBoards = (category: CategoryBoards): Board[] => {
const categoryBoardsByID = new Map<string, Board>()
boards.forEach((board) => {
if (!category.boardIDs.includes(board.id)) {
if (!category.boardMetadata.find((m) => m.boardID === board.id)) {
return
}
@ -321,8 +334,8 @@ const Sidebar = (props: Props) => {
})
const sortedBoards: Board[] = []
category.boardIDs.forEach((boardID) => {
const b = categoryBoardsByID.get(boardID)
category.boardMetadata.forEach((boardMetadata) => {
const b = categoryBoardsByID.get(boardMetadata.boardID)
if (b) {
sortedBoards.push(b)
}

View File

@ -28,7 +28,7 @@ describe('components/sidebarBoardItem', () => {
const categoryBoards1 = TestBlockFactory.createCategoryBoards()
categoryBoards1.name = 'Category 1'
categoryBoards1.boardIDs = [board.id]
categoryBoards1.boardMetadata = [{boardID: board.id, hidden: false}]
const categoryBoards2 = TestBlockFactory.createCategoryBoards()
categoryBoards2.name = 'Category 2'

View File

@ -35,10 +35,9 @@ import {Utils} from '../../utils'
import AddIcon from '../../widgets/icons/add'
import CloseIcon from '../../widgets/icons/close'
import {UserConfigPatch} from '../../user'
import {getMe, getMyConfig, patchProps} from '../../store/users'
import {getMe} from '../../store/users'
import octoClient from '../../octoClient'
import {getCurrentBoardId, getMySortedBoards} from '../../store/boards'
import {getCurrentBoardId} from '../../store/boards'
import {UserSettings} from '../../userSettings'
import {Archiver} from '../../archiver'
@ -75,12 +74,10 @@ const SidebarBoardItem = (props: Props) => {
const currentViewId = useAppSelector(getCurrentViewId)
const teamID = team?.id || ''
const me = useAppSelector(getMe)
const myConfig = useAppSelector(getMyConfig)
const match = useRouteMatch<{boardId: string, viewId?: string, cardId?: string, teamId?: string}>()
const history = useHistory()
const dispatch = useAppDispatch()
const myAllBoards = useAppSelector(getMySortedBoards)
const currentBoardID = useAppSelector(getCurrentBoardId)
const generateMoveToCategoryOptions = (boardID: string) => {
@ -136,6 +133,7 @@ const SidebarBoardItem = (props: Props) => {
await dispatch(updateBoardCategories([{
boardID: boardId,
categoryID: props.categoryBoards.id,
hidden: false,
}]))
}
@ -155,23 +153,14 @@ const SidebarBoardItem = (props: Props) => {
return
}
// creating new array as myConfig.hiddenBoardIDs.value
// belongs to Redux state and so is immutable.
const hiddenBoards = {...(myConfig.hiddenBoardIDs ? myConfig.hiddenBoardIDs.value : {})}
hiddenBoards[board.id] = true
const hiddenBoardsArray = Object.keys(hiddenBoards)
const patch: UserConfigPatch = {
updatedFields: {
hiddenBoardIDs: JSON.stringify(hiddenBoardsArray),
await octoClient.hideBoard(props.categoryBoards.id, board.id)
dispatch(updateBoardCategories([
{
boardID: board.id,
categoryID: props.categoryBoards.id,
hidden: true,
},
}
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
if (!patchedProps) {
return
}
await dispatch(patchProps(patchedProps))
]))
// If we're hiding the board we're currently on,
// we need to switch to a different board once its hidden.
@ -181,17 +170,22 @@ const SidebarBoardItem = (props: Props) => {
// Empty board ID navigates to template picker, which is
// fine if there are no more visible boards to switch to.
const visibleBoards = myAllBoards.filter((b) => !hiddenBoards[b.id])
if (visibleBoards.length === 0) {
// find the first visible board
let visibleBoardID: string | null = null
for (const iterCategory of props.allCategories) {
const visibleBoardMetadata = iterCategory.boardMetadata.find((categoryBoardMetadata) => !categoryBoardMetadata.hidden && categoryBoardMetadata.boardID !== props.board.id)
if (visibleBoardMetadata) {
visibleBoardID = visibleBoardMetadata.boardID
break
}
}
if (visibleBoardID === null) {
UserSettings.setLastBoardID(match.params.teamId!, null)
showTemplatePicker()
} else {
let nextBoardID = ''
if (visibleBoards.length > 0) {
nextBoardID = visibleBoards[0].id
}
props.showBoard(nextBoardID)
props.showBoard(visibleBoardID)
}
}
}

View File

@ -36,7 +36,7 @@ describe('components/sidebarCategory', () => {
const categoryBoards1 = TestBlockFactory.createCategoryBoards()
categoryBoards1.id = 'category_1_id'
categoryBoards1.name = 'Category 1'
categoryBoards1.boardIDs = [board1.id, board2.id]
categoryBoards1.boardMetadata = [{boardID: board1.id, hidden: false}, {boardID: board2.id, hidden: false}]
const categoryBoards2 = TestBlockFactory.createCategoryBoards()
categoryBoards2.id = 'category_2_id'

View File

@ -20,14 +20,13 @@ import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import './sidebarCategory.scss'
import {Category, CategoryBoards} from '../../store/sidebar'
import {Category, CategoryBoardMetadata, CategoryBoards} from '../../store/sidebar'
import ChevronDown from '../../widgets/icons/chevronDown'
import ChevronRight from '../../widgets/icons/chevronRight'
import CreateNewFolder from '../../widgets/icons/newFolder'
import CreateCategory from '../createCategory/createCategory'
import {useAppSelector} from '../../store/hooks'
import {
getMyConfig,
getOnboardingTourCategory,
getOnboardingTourStep,
} from '../../store/users'
@ -76,7 +75,6 @@ const SidebarCategory = (props: Props) => {
const match = useRouteMatch<{boardId: string, viewId?: string, cardId?: string, teamId?: string}>()
const [showCreateCategoryModal, setShowCreateCategoryModal] = useState(false)
const [showUpdateCategoryModal, setShowUpdateCategoryModal] = useState(false)
const myConfig = useAppSelector(getMyConfig)
const onboardingTourCategory = useAppSelector(getOnboardingTourCategory)
const onboardingTourStep = useAppSelector(getOnboardingTourStep)
@ -129,19 +127,20 @@ const SidebarCategory = (props: Props) => {
props.hideSidebar()
}, [match, history])
const isBoardVisible = (boardID: string): boolean => {
const isBoardVisible = (boardID: string, existingBoardMetadata?: CategoryBoardMetadata): boolean => {
const categoryBoardMetadata = existingBoardMetadata || sidebarBoardMetadata.find((metadata) => metadata.boardID === boardID)
// hide if board doesn't belong to current category
if (!blocks.includes(boardID)) {
if (!categoryBoardMetadata) {
return false
}
// hide if board was hidden by the user
const hiddenBoardIDs = myConfig.hiddenBoardIDs?.value || {}
return !hiddenBoardIDs[boardID]
return !categoryBoardMetadata.hidden
}
const blocks = props.categoryBoards.boardIDs || []
const visibleBlocks = props.categoryBoards.boardIDs.filter((boardID) => isBoardVisible(boardID))
const sidebarBoardMetadata = props.categoryBoards.boardMetadata || []
const visibleBlocks = props.categoryBoards.boardMetadata.filter((boardMetadata) => isBoardVisible(boardMetadata.boardID, boardMetadata))
const handleCreateNewCategory = () => {
setShowCreateCategoryModal(true)

View File

@ -92,7 +92,7 @@ const me: IUser = {
const categoryAttribute1 = TestBlockFactory.createCategoryBoards()
categoryAttribute1.name = 'Category 1'
categoryAttribute1.boardIDs = [board.id]
categoryAttribute1.boardMetadata = [{boardID: board.id, hidden: false}]
jest.mock('react-router-dom', () => {
const originalModule = jest.requireActual('react-router-dom')
@ -171,6 +171,7 @@ describe('src/components/workspace', () => {
categoryAttributes: [
categoryAttribute1,
],
hiddenBoardIDs: [],
},
}
mockedOctoClient.searchTeamUsers.mockResolvedValue(Object.values(state.users.boardUsers))
@ -285,6 +286,12 @@ describe('src/components/workspace', () => {
featureFlags: {},
},
},
sidebar: {
categoryAttributes: [
categoryAttribute1,
],
hiddenBoardIDs: [],
},
})
let container: Element | undefined
await act(async () => {
@ -388,6 +395,7 @@ describe('src/components/workspace', () => {
categoryAttributes: [
categoryAttribute1,
],
hiddenBoardIDs: [],
},
}
const localStore = mockStateStore([thunk], localState)
@ -492,6 +500,7 @@ describe('src/components/workspace', () => {
categoryAttributes: [
categoryAttribute1,
],
hiddenBoardIDs: [],
},
}
const localStore = mockStateStore([thunk], localState)
@ -601,6 +610,7 @@ describe('src/components/workspace', () => {
categoryAttributes: [
categoryAttribute1,
],
hiddenBoardIDs: [],
},
}
const localStore = mockStateStore([thunk], localState)

View File

@ -25,7 +25,9 @@ import {Utils} from '../utils'
import {IUser} from '../user'
import propsRegistry from '../properties'
import {getMe, getMyConfig} from '../store/users'
import {getMe} from '../store/users'
import {getHiddenBoardIDs} from '../store/sidebar'
import CenterPanel from './centerPanel'
import BoardTemplateSelector from './boardTemplateSelector/boardTemplateSelector'
@ -54,12 +56,11 @@ function CenterContent(props: Props) {
const cardLimitTimestamp = useAppSelector(getCardLimitTimestamp)
const history = useHistory()
const dispatch = useAppDispatch()
const myConfig = useAppSelector(getMyConfig)
const me = useAppSelector<IUser|null>(getMe)
const hiddenBoardIDs = useAppSelector(getHiddenBoardIDs)
const isBoardHidden = () => {
const hiddenBoardIDs = myConfig.hiddenBoardIDs?.value || {}
return hiddenBoardIDs[board.id]
return hiddenBoardIDs.includes(board.id)
}
const showCard = useCallback((cardId?: string) => {

View File

@ -1033,6 +1033,22 @@ class OctoClient {
body: '{}',
})
}
async hideBoard(categoryID: string, boardID: string): Promise<Response> {
const path = `${this.teamPath()}/categories/${categoryID}/boards/${boardID}/hide`
return fetch(this.getBaseURL() + path, {
method: 'PUT',
headers: this.headers(),
})
}
async unhideBoard(categoryID: string, boardID: string): Promise<Response> {
const path = `${this.teamPath()}/categories/${categoryID}/boards/${boardID}/unhide`
return fetch(this.getBaseURL() + path, {
method: 'PUT',
headers: this.headers(),
})
}
}
const octoClient = new OctoClient()

View File

@ -12,7 +12,7 @@ import octoClient from '../../octoClient'
import {Subscription, WSClient} from '../../wsclient'
import {Utils} from '../../utils'
import {useWebsockets} from '../../hooks/websockets'
import {IUser, UserConfigPatch} from '../../user'
import {IUser} from '../../user'
import {Block} from '../../blocks/block'
import {ContentBlock} from '../../blocks/contentBlock'
import {CommentBlock} from '../../blocks/commentBlock'
@ -40,7 +40,7 @@ import {
fetchUserBlockSubscriptions,
getMe,
followBlock,
unfollowBlock, patchProps, getMyConfig,
unfollowBlock,
} from '../../store/users'
import {setGlobalError} from '../../store/globalError'
import {UserSettings} from '../../userSettings'
@ -52,6 +52,8 @@ import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../teleme
import {Constants} from '../../constants'
import {getCategoryOfBoard, getHiddenBoardIDs} from '../../store/sidebar'
import SetWindowTitleAndIcon from './setWindowTitleAndIcon'
import TeamToBoardAndViewRedirect from './teamToBoardAndViewRedirect'
import UndoRedoHotKeys from './undoRedoHotKeys'
@ -75,7 +77,8 @@ const BoardPage = (props: Props): JSX.Element => {
const teamId = match.params.teamId || UserSettings.lastTeamId || Constants.globalTeamId
const viewId = match.params.viewId
const me = useAppSelector<IUser|null>(getMe)
const myConfig = useAppSelector(getMyConfig)
const hiddenBoardIDs = useAppSelector(getHiddenBoardIDs)
const category = useAppSelector(getCategoryOfBoard(activeBoardId))
// if we're in a legacy route and not showing a shared board,
// redirect to the new URL schema equivalent
@ -220,28 +223,11 @@ const BoardPage = (props: Props): JSX.Element => {
}, [teamId, match.params.boardId, viewId, me?.id])
const handleUnhideBoard = async (boardID: string) => {
Utils.log('handleUnhideBoard called')
if (!me) {
if (!me || !category) {
return
}
const hiddenBoards = {...(myConfig.hiddenBoardIDs ? myConfig.hiddenBoardIDs.value : {})}
// const index = hiddenBoards.indexOf(boardID)
// hiddenBoards.splice(index, 1)
delete hiddenBoards[boardID]
const hiddenBoardsArray = Object.keys(hiddenBoards)
const patch: UserConfigPatch = {
updatedFields: {
hiddenBoardIDs: JSON.stringify(hiddenBoardsArray),
},
}
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
if (!patchedProps) {
return
}
await dispatch(patchProps(patchedProps))
await octoClient.unhideBoard(category.id, boardID)
}
useEffect(() => {
@ -249,8 +235,7 @@ const BoardPage = (props: Props): JSX.Element => {
return
}
const hiddenBoardIDs = myConfig.hiddenBoardIDs?.value || {}
if (hiddenBoardIDs[match.params.boardId]) {
if (hiddenBoardIDs.indexOf(match.params.boardId) >= 0) {
handleUnhideBoard(match.params.boardId)
}
}, [me?.id, teamId, match.params.boardId])

View File

@ -32,10 +32,10 @@ const TeamToBoardAndViewRedirect = (): null => {
let goToBoardID: string | null = null
for (const category of categories) {
for (const categoryBoardID of category.boardIDs) {
if (boards[categoryBoardID]) {
// pick the first category board that exists
goToBoardID = categoryBoardID
for (const boardMetadata of category.boardMetadata) {
// pick the first category board that exists and is not hidden
if (!boardMetadata.hidden && boards[boardMetadata.boardID]) {
goToBoardID = boardMetadata.boardID
break
}
}

View File

@ -25,18 +25,24 @@ interface Category {
isNew: boolean
}
interface CategoryBoardMetadata {
boardID: string
hidden: boolean
}
interface CategoryBoards extends Category {
boardIDs: string[]
boardMetadata: CategoryBoardMetadata[]
}
interface BoardCategoryWebsocketData {
boardID: string
categoryID: string
hidden: boolean
}
interface CategoryBoardsReorderData {
categoryID: string
boardIDs: string[]
boardsMetadata: CategoryBoardMetadata[]
}
export const DefaultCategory: CategoryBoards = {
@ -53,11 +59,12 @@ export const fetchSidebarCategories = createAsyncThunk(
type Sidebar = {
categoryAttributes: CategoryBoards[]
hiddenBoardIDs: string[]
}
const sidebarSlice = createSlice({
name: 'sidebar',
initialState: {categoryAttributes: []} as Sidebar,
initialState: {categoryAttributes: [], hiddenBoardIDs: []} as Sidebar,
reducers: {
updateCategories: (state, action: PayloadAction<Category[]>) => {
action.payload.forEach((updatedCategory) => {
@ -68,7 +75,7 @@ const sidebarSlice = createSlice({
// new categories should always show up on the top
state.categoryAttributes.unshift({
...updatedCategory,
boardIDs: [],
boardMetadata: [],
isNew: true,
})
} else if (updatedCategory.deleteAt) {
@ -87,31 +94,44 @@ const sidebarSlice = createSlice({
},
updateBoardCategories: (state, action: PayloadAction<BoardCategoryWebsocketData[]>) => {
const updatedCategoryAttributes: CategoryBoards[] = []
let updatedHiddenBoardIDs = state.hiddenBoardIDs
action.payload.forEach((boardCategory) => {
for (let i = 0; i < state.categoryAttributes.length; i++) {
const categoryAttribute = state.categoryAttributes[i]
if (categoryAttribute.id === boardCategory.categoryID) {
// if board is already in the right category, don't do anything
// and let the board stay in its right order.
// Only if its not in the right category, do add it.
if (categoryAttribute.boardIDs.indexOf(boardCategory.boardID) < 0) {
categoryAttribute.boardIDs.unshift(boardCategory.boardID)
const categoryBoardMetadataIndex = categoryAttribute.boardMetadata.findIndex((boardMetadata) => boardMetadata.boardID === boardCategory.boardID)
if (categoryBoardMetadataIndex >= 0) {
categoryAttribute.boardMetadata[categoryBoardMetadataIndex] = {
...categoryAttribute.boardMetadata[categoryBoardMetadataIndex],
hidden: boardCategory.hidden,
}
} else {
categoryAttribute.boardMetadata.unshift({boardID: boardCategory.boardID, hidden: boardCategory.hidden})
categoryAttribute.isNew = false
}
} else {
// remove the board from other categories
categoryAttribute.boardIDs = categoryAttribute.boardIDs.filter((boardID) => boardID !== boardCategory.boardID)
categoryAttribute.boardMetadata = categoryAttribute.boardMetadata.filter((metadata) => metadata.boardID !== boardCategory.boardID)
}
updatedCategoryAttributes[i] = categoryAttribute
if (boardCategory.hidden) {
if (updatedHiddenBoardIDs.indexOf(boardCategory.boardID) < 0) {
updatedHiddenBoardIDs.push(boardCategory.boardID)
}
} else {
updatedHiddenBoardIDs = updatedHiddenBoardIDs.filter((hiddenBoardID) => hiddenBoardID !== boardCategory.boardID)
}
}
})
if (updatedCategoryAttributes.length > 0) {
state.categoryAttributes = updatedCategoryAttributes
}
state.hiddenBoardIDs = updatedHiddenBoardIDs
},
updateCategoryOrder: (state, action: PayloadAction<string[]>) => {
if (action.payload.length === 0) {
@ -134,7 +154,7 @@ const sidebarSlice = createSlice({
state.categoryAttributes = newOrderedCategories
},
updateCategoryBoardsOrder: (state, action: PayloadAction<CategoryBoardsReorderData>) => {
if (action.payload.boardIDs.length === 0) {
if (action.payload.boardsMetadata.length === 0) {
return
}
@ -145,9 +165,9 @@ const sidebarSlice = createSlice({
}
const category = state.categoryAttributes[categoryIndex]
const updatedCategory = {
const updatedCategory: CategoryBoards = {
...category,
boardIDs: action.payload.boardIDs,
boardMetadata: action.payload.boardsMetadata,
isNew: false,
}
@ -158,6 +178,17 @@ const sidebarSlice = createSlice({
extraReducers: (builder) => {
builder.addCase(fetchSidebarCategories.fulfilled, (state, action) => {
state.categoryAttributes = action.payload || []
state.hiddenBoardIDs = state.categoryAttributes.flatMap(
(ca) => {
return ca.boardMetadata.reduce((collector, m) => {
if (m.hidden) {
collector.push(m.boardID)
}
return collector
}, [] as string[])
},
)
})
},
})
@ -167,9 +198,18 @@ export const getSidebarCategories = createSelector(
(sidebarCategories) => sidebarCategories,
)
export const getHiddenBoardIDs = (state: RootState): string[] => state.sidebar.hiddenBoardIDs
export function getCategoryOfBoard(boardID: string): (state: RootState) => CategoryBoards | undefined {
return createSelector(
(state: RootState): CategoryBoards[] => state.sidebar.categoryAttributes,
(sidebarCategories) => sidebarCategories.find((category) => category.boardMetadata.findIndex((m) => m.boardID === boardID) >= 0),
)
}
export const {reducer} = sidebarSlice
export const {updateCategories, updateBoardCategories, updateCategoryOrder, updateCategoryBoardsOrder} = sidebarSlice.actions
export {Category, CategoryBoards, BoardCategoryWebsocketData, CategoryBoardsReorderData}
export {Category, CategoryBoards, BoardCategoryWebsocketData, CategoryBoardsReorderData, CategoryBoardMetadata}

View File

@ -182,7 +182,7 @@ class TestBlockFactory {
static createCategoryBoards(): CategoryBoards {
return {
...TestBlockFactory.createCategory(),
boardIDs: [],
boardMetadata: [],
}
}