Boards as persisted category (#3877)
* WIP * WIP * Removed unused webapp util * Added server tests * Lint fix * Updating existing tests * Updating existing tests * Updating existing tests * Fixing existing tests * Fixing existing tests * Fixing existing tests * Added category type and tests * updated tests * Fixed integration test * type fix * removed seconds from boards name * wip * debugging cy test * Fixed a bug preventing users from collapsing boards category * Debugging cypress test * CI * debugging cy test * Testing a fix * reverting test fix * Handled personal server * Fixed a case for personal server * fixed a test
This commit is contained in:
parent
ba792191cd
commit
8d17dd820e
27 changed files with 746 additions and 304 deletions
|
@ -178,6 +178,7 @@ func (a *API) userIsGuest(userID string) (bool, error) {
|
|||
// Response helpers
|
||||
|
||||
func (a *API) errorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||
a.logger.Error(err.Error())
|
||||
errorResponse := model.ErrorResponse{Error: err.Error()}
|
||||
|
||||
switch {
|
||||
|
|
|
@ -489,6 +489,10 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if toTeam == "" {
|
||||
toTeam = board.TeamID
|
||||
}
|
||||
|
||||
if toTeam == "" && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
|
||||
return
|
||||
|
|
|
@ -21,6 +21,8 @@ var (
|
|||
const linkBoardMessage = "@%s linked the board [%s](%s) with this channel"
|
||||
const unlinkBoardMessage = "@%s unlinked the board [%s](%s) with this channel"
|
||||
|
||||
var errNoDefaultCategoryFound = errors.New("no default category found for user")
|
||||
|
||||
func (a *App) GetBoard(boardID string) (*model.Board, error) {
|
||||
board, err := a.store.GetBoard(boardID)
|
||||
if err != nil {
|
||||
|
@ -142,7 +144,7 @@ func (a *App) getBoardDescendantModifiedInfo(boardID string, latest bool) (int64
|
|||
return timestamp, modifiedBy, nil
|
||||
}
|
||||
|
||||
func (a *App) setBoardCategoryFromSource(sourceBoardID, destinationBoardID, userID, teamID string) error {
|
||||
func (a *App) setBoardCategoryFromSource(sourceBoardID, destinationBoardID, userID, teamID string, asTemplate bool) error {
|
||||
// find source board's category ID for the user
|
||||
userCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID)
|
||||
if err != nil {
|
||||
|
@ -161,10 +163,14 @@ func (a *App) setBoardCategoryFromSource(sourceBoardID, destinationBoardID, user
|
|||
}
|
||||
}
|
||||
|
||||
// if source board is not mapped to a category for this user,
|
||||
// then we have nothing more to do.
|
||||
if destinationCategoryID == "" {
|
||||
return nil
|
||||
// if source board is not mapped to a category for this user,
|
||||
// then move new board to default category
|
||||
if !asTemplate {
|
||||
return a.addBoardsToDefaultCategory(userID, teamID, []*model.Board{{ID: destinationBoardID}})
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// now that we have source board's category,
|
||||
|
@ -184,7 +190,7 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
|
|||
}
|
||||
|
||||
for _, board := range bab.Boards {
|
||||
if categoryErr := a.setBoardCategoryFromSource(boardID, board.ID, userID, board.TeamID); categoryErr != nil {
|
||||
if categoryErr := a.setBoardCategoryFromSource(boardID, board.ID, userID, toTeam, asTemplate); categoryErr != nil {
|
||||
return nil, nil, categoryErr
|
||||
}
|
||||
}
|
||||
|
@ -294,9 +300,42 @@ func (a *App) CreateBoard(board *model.Board, userID string, addMember bool) (*m
|
|||
return nil
|
||||
})
|
||||
|
||||
if board.TeamID != "0" {
|
||||
if err := a.addBoardsToDefaultCategory(userID, newBoard.TeamID, []*model.Board{newBoard}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return newBoard, nil
|
||||
}
|
||||
|
||||
func (a *App) addBoardsToDefaultCategory(userID, teamID string, boards []*model.Board) error {
|
||||
userCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultCategoryID := ""
|
||||
for _, categoryBoard := range userCategoryBoards {
|
||||
if categoryBoard.Name == defaultCategoryBoards {
|
||||
defaultCategoryID = categoryBoard.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if defaultCategoryID == "" {
|
||||
return fmt.Errorf("%w userID: %s", errNoDefaultCategoryFound, userID)
|
||||
}
|
||||
|
||||
for _, board := range boards {
|
||||
if err := a.AddUpdateUserCategoryBoard(teamID, userID, defaultCategoryID, board.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*model.Board, error) {
|
||||
var oldChannelID string
|
||||
var isTemplate bool
|
||||
|
|
|
@ -55,6 +55,14 @@ func (a *App) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string, a
|
|||
}()
|
||||
}
|
||||
|
||||
for _, board := range newBab.Boards {
|
||||
if !board.IsTemplate {
|
||||
if err := a.addBoardsToDefaultCategory(userID, board.TeamID, []*model.Board{board}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newBab, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,10 @@ package app
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -378,3 +382,46 @@ func TestGetBoardCount(t *testing.T) {
|
|||
require.Equal(t, boardCount, count)
|
||||
})
|
||||
}
|
||||
|
||||
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"},
|
||||
},
|
||||
{
|
||||
Category: model.Category{ID: "category_id_2", Name: "Category 2"},
|
||||
BoardIDs: []string{"board_id_3"},
|
||||
},
|
||||
{
|
||||
Category: model.Category{ID: "category_id_3", Name: "Category 3"},
|
||||
BoardIDs: []string{},
|
||||
},
|
||||
}, 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().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, nil)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "default_category_id", "board_id_1").Return(nil)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "default_category_id", "board_id_2").Return(nil)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "default_category_id", "board_id_3").Return(nil)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
)
|
||||
|
||||
var ErrCannotDeleteSystemCategory = errors.New("cannot delete a system category")
|
||||
var ErrCannotUpdateSystemCategory = errors.New("cannot update a system category")
|
||||
|
||||
func (a *App) CreateCategory(category *model.Category) (*model.Category, error) {
|
||||
category.Hydrate()
|
||||
if err := category.IsValid(); err != nil {
|
||||
|
@ -28,6 +33,10 @@ func (a *App) CreateCategory(category *model.Category) (*model.Category, error)
|
|||
}
|
||||
|
||||
func (a *App) UpdateCategory(category *model.Category) (*model.Category, error) {
|
||||
if err := category.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// verify if category belongs to the user
|
||||
existingCategory, err := a.store.GetCategory(category.ID)
|
||||
if err != nil {
|
||||
|
@ -42,6 +51,17 @@ func (a *App) UpdateCategory(category *model.Category) (*model.Category, error)
|
|||
return nil, model.ErrCategoryPermissionDenied
|
||||
}
|
||||
|
||||
if existingCategory.TeamID != category.TeamID {
|
||||
return nil, model.ErrCategoryPermissionDenied
|
||||
}
|
||||
|
||||
if existingCategory.Type == model.CategoryTypeSystem {
|
||||
// You cannot rename or delete a system category,
|
||||
// So restoring its name and undeleting it if set so.
|
||||
category.Name = existingCategory.Name
|
||||
category.DeleteAt = 0
|
||||
}
|
||||
|
||||
category.UpdateAt = utils.GetMillis()
|
||||
if err = category.IsValid(); err != nil {
|
||||
return nil, err
|
||||
|
@ -84,6 +104,10 @@ func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category
|
|||
return nil, model.NewErrInvalidCategory("category doesn't belong to the team")
|
||||
}
|
||||
|
||||
if existingCategory.Type == model.CategoryTypeSystem {
|
||||
return nil, ErrCannotDeleteSystemCategory
|
||||
}
|
||||
|
||||
if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1,9 +1,105 @@
|
|||
package app
|
||||
|
||||
import "github.com/mattermost/focalboard/server/model"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
)
|
||||
|
||||
const defaultCategoryBoards = "Boards"
|
||||
|
||||
func (a *App) GetUserCategoryBoards(userID, teamID string) ([]model.CategoryBoards, error) {
|
||||
return a.store.GetUserCategoryBoards(userID, teamID)
|
||||
categoryBoards, err := a.store.GetUserCategoryBoards(userID, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createdCategoryBoards, err := a.createDefaultCategoriesIfRequired(categoryBoards, userID, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
categoryBoards = append(categoryBoards, createdCategoryBoards...)
|
||||
return categoryBoards, nil
|
||||
}
|
||||
|
||||
func (a *App) createDefaultCategoriesIfRequired(existingCategoryBoards []model.CategoryBoards, userID, teamID string) ([]model.CategoryBoards, error) {
|
||||
createdCategories := []model.CategoryBoards{}
|
||||
|
||||
boardsCategoryExist := false
|
||||
for _, categoryBoard := range existingCategoryBoards {
|
||||
if categoryBoard.Name == defaultCategoryBoards {
|
||||
boardsCategoryExist = true
|
||||
}
|
||||
}
|
||||
|
||||
if !boardsCategoryExist {
|
||||
createdCategoryBoards, err := a.createBoardsCategory(userID, teamID, existingCategoryBoards)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createdCategories = append(createdCategories, *createdCategoryBoards)
|
||||
}
|
||||
|
||||
return createdCategories, nil
|
||||
}
|
||||
|
||||
func (a *App) createBoardsCategory(userID, teamID string, existingCategoryBoards []model.CategoryBoards) (*model.CategoryBoards, error) {
|
||||
// create the category
|
||||
category := model.Category{
|
||||
Name: defaultCategoryBoards,
|
||||
UserID: userID,
|
||||
TeamID: teamID,
|
||||
Collapsed: false,
|
||||
Type: model.CategoryTypeSystem,
|
||||
}
|
||||
createdCategory, err := a.CreateCategory(&category)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("createBoardsCategory default category creation failed: %w", err)
|
||||
}
|
||||
|
||||
// once the category is created, we need to move all boards which do not
|
||||
// belong to any category, into this category.
|
||||
|
||||
userBoards, err := a.GetBoardsForUserAndTeam(userID, teamID, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("createBoardsCategory error fetching user's team's boards: %w", err)
|
||||
}
|
||||
|
||||
createdCategoryBoards := &model.CategoryBoards{
|
||||
Category: *createdCategory,
|
||||
BoardIDs: []string{},
|
||||
}
|
||||
|
||||
for _, board := range userBoards {
|
||||
belongsToCategory := false
|
||||
|
||||
for _, categoryBoard := range existingCategoryBoards {
|
||||
for _, boardID := range categoryBoard.BoardIDs {
|
||||
if boardID == board.ID {
|
||||
belongsToCategory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// stop looking into other categories if
|
||||
// the board was found in a category
|
||||
if belongsToCategory {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !belongsToCategory {
|
||||
if err := a.AddUpdateUserCategoryBoard(teamID, userID, createdCategory.ID, board.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)
|
||||
}
|
||||
|
||||
createdCategoryBoards.BoardIDs = append(createdCategoryBoards.BoardIDs, board.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return createdCategoryBoards, nil
|
||||
}
|
||||
|
||||
func (a *App) AddUpdateUserCategoryBoard(teamID, userID, categoryID, boardID string) error {
|
||||
|
|
82
server/app/category_boards_test.go
Normal file
82
server/app/category_boards_test.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetUserCategoryBoards(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(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().CreateCategory(utils.Anything).Return(nil)
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "boards_category_id",
|
||||
Name: "Boards",
|
||||
}, nil)
|
||||
|
||||
board1 := &model.Board{
|
||||
ID: "board_id_1",
|
||||
}
|
||||
|
||||
board2 := &model.Board{
|
||||
ID: "board_id_2",
|
||||
}
|
||||
|
||||
board3 := &model.Board{
|
||||
ID: "board_id_3",
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{board1, board2, board3}, nil)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", "board_id_1").Return(nil)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", "board_id_2").Return(nil)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", "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")
|
||||
})
|
||||
|
||||
t.Run("user had no default category BUT had no boards", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{}, nil)
|
||||
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil)
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "boards_category_id",
|
||||
Name: "Boards",
|
||||
}, nil)
|
||||
|
||||
th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, 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, 0, len(categoryBoards[0].BoardIDs))
|
||||
})
|
||||
|
||||
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"},
|
||||
},
|
||||
}, 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, 2, len(categoryBoards[0].BoardIDs))
|
||||
})
|
||||
}
|
295
server/app/category_test.go
Normal file
295
server/app/category_test.go
Normal file
|
@ -0,0 +1,295 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateCategory(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
}, nil)
|
||||
|
||||
category := &model.Category{
|
||||
Name: "Category",
|
||||
UserID: "user_id",
|
||||
TeamID: "team_id",
|
||||
Type: "custom",
|
||||
}
|
||||
createdCategory, err := th.App.CreateCategory(category)
|
||||
assert.NotNil(t, createdCategory)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("creating invalid category", func(t *testing.T) {
|
||||
category := &model.Category{
|
||||
Name: "", // empty name shouldn't be allowed
|
||||
UserID: "user_id",
|
||||
TeamID: "team_id",
|
||||
Type: "custom",
|
||||
}
|
||||
createdCategory, err := th.App.CreateCategory(category)
|
||||
assert.Nil(t, createdCategory)
|
||||
assert.Error(t, err)
|
||||
|
||||
category.Name = "Name"
|
||||
category.UserID = "" // empty creator user id shouldn't be allowed
|
||||
createdCategory, err = th.App.CreateCategory(category)
|
||||
assert.Nil(t, createdCategory)
|
||||
assert.Error(t, err)
|
||||
|
||||
category.UserID = "user_id"
|
||||
category.TeamID = "" // empty TeamID shouldn't be allowed
|
||||
createdCategory, err = th.App.CreateCategory(category)
|
||||
assert.Nil(t, createdCategory)
|
||||
assert.Error(t, err)
|
||||
|
||||
category.Type = "invalid" // unknown type shouldn't be allowed
|
||||
createdCategory, err = th.App.CreateCategory(category)
|
||||
assert.Nil(t, createdCategory)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateCategory(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
TeamID: "team_id_1",
|
||||
UserID: "user_id_1",
|
||||
Type: "custom",
|
||||
}, nil)
|
||||
|
||||
th.Store.EXPECT().UpdateCategory(utils.Anything).Return(nil)
|
||||
th.Store.EXPECT().GetCategory("category_id_1").Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
}, nil)
|
||||
|
||||
category := &model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
UserID: "user_id_1",
|
||||
TeamID: "team_id_1",
|
||||
Type: "custom",
|
||||
}
|
||||
updatedCategory, err := th.App.UpdateCategory(category)
|
||||
assert.NotNil(t, updatedCategory)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("updating invalid category", func(t *testing.T) {
|
||||
category := &model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Name",
|
||||
UserID: "user_id",
|
||||
TeamID: "team_id",
|
||||
Type: "custom",
|
||||
}
|
||||
|
||||
category.ID = ""
|
||||
createdCategory, err := th.App.UpdateCategory(category)
|
||||
assert.Nil(t, createdCategory)
|
||||
assert.Error(t, err)
|
||||
|
||||
category.ID = "category_id_1"
|
||||
category.Name = ""
|
||||
createdCategory, err = th.App.UpdateCategory(category)
|
||||
assert.Nil(t, createdCategory)
|
||||
assert.Error(t, err)
|
||||
|
||||
category.Name = "Name"
|
||||
category.UserID = "" // empty creator user id shouldn't be allowed
|
||||
createdCategory, err = th.App.UpdateCategory(category)
|
||||
assert.Nil(t, createdCategory)
|
||||
assert.Error(t, err)
|
||||
|
||||
category.UserID = "user_id"
|
||||
category.TeamID = "" // empty TeamID shouldn't be allowed
|
||||
createdCategory, err = th.App.UpdateCategory(category)
|
||||
assert.Nil(t, createdCategory)
|
||||
assert.Error(t, err)
|
||||
|
||||
category.Type = "invalid" // unknown type shouldn't be allowed
|
||||
createdCategory, err = th.App.UpdateCategory(category)
|
||||
assert.Nil(t, createdCategory)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("trying to update someone else's category", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
TeamID: "team_id_1",
|
||||
UserID: "user_id_1",
|
||||
Type: "custom",
|
||||
}, nil)
|
||||
|
||||
category := &model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
UserID: "user_id_2",
|
||||
TeamID: "team_id_1",
|
||||
Type: "custom",
|
||||
}
|
||||
updatedCategory, err := th.App.UpdateCategory(category)
|
||||
assert.Nil(t, updatedCategory)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("trying to update some other team's category", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
TeamID: "team_id_1",
|
||||
UserID: "user_id_1",
|
||||
Type: "custom",
|
||||
}, nil)
|
||||
|
||||
category := &model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
UserID: "user_id_1",
|
||||
TeamID: "team_id_2",
|
||||
Type: "custom",
|
||||
}
|
||||
updatedCategory, err := th.App.UpdateCategory(category)
|
||||
assert.Nil(t, updatedCategory)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("should not be allowed to rename system category", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
TeamID: "team_id_1",
|
||||
UserID: "user_id_1",
|
||||
Type: "system",
|
||||
}, nil).Times(1)
|
||||
|
||||
th.Store.EXPECT().UpdateCategory(utils.Anything).Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
TeamID: "team_id_1",
|
||||
UserID: "user_id_1",
|
||||
Type: "system",
|
||||
Collapsed: true,
|
||||
}, nil).Times(1)
|
||||
|
||||
category := &model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Updated Name",
|
||||
UserID: "user_id_1",
|
||||
TeamID: "team_id_1",
|
||||
Type: "system",
|
||||
}
|
||||
updatedCategory, err := th.App.UpdateCategory(category)
|
||||
assert.NotNil(t, updatedCategory)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Category", updatedCategory.Name)
|
||||
})
|
||||
|
||||
t.Run("should be allowed to collapse and expand any category type", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
TeamID: "team_id_1",
|
||||
UserID: "user_id_1",
|
||||
Type: "system",
|
||||
Collapsed: false,
|
||||
}, nil).Times(1)
|
||||
|
||||
th.Store.EXPECT().UpdateCategory(utils.Anything).Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Category",
|
||||
TeamID: "team_id_1",
|
||||
UserID: "user_id_1",
|
||||
Type: "system",
|
||||
Collapsed: true,
|
||||
}, nil).Times(1)
|
||||
|
||||
category := &model.Category{
|
||||
ID: "category_id_1",
|
||||
Name: "Updated Name",
|
||||
UserID: "user_id_1",
|
||||
TeamID: "team_id_1",
|
||||
Type: "system",
|
||||
Collapsed: true,
|
||||
}
|
||||
updatedCategory, err := th.App.UpdateCategory(category)
|
||||
assert.NotNil(t, updatedCategory)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Category", updatedCategory.Name, "The name should have not been updated")
|
||||
assert.True(t, updatedCategory.Collapsed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteCategory(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetCategory("category_id_1").Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
DeleteAt: 0,
|
||||
UserID: "user_id_1",
|
||||
TeamID: "team_id_1",
|
||||
Type: "custom",
|
||||
}, nil)
|
||||
|
||||
th.Store.EXPECT().DeleteCategory("category_id_1", "user_id_1", "team_id_1").Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetCategory("category_id_1").Return(&model.Category{
|
||||
DeleteAt: 10000,
|
||||
}, nil)
|
||||
|
||||
deletedCategory, err := th.App.DeleteCategory("category_id_1", "user_id_1", "team_id_1")
|
||||
assert.NotNil(t, deletedCategory)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("trying to delete already deleted category", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetCategory("category_id_1").Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
DeleteAt: 1000,
|
||||
UserID: "user_id_1",
|
||||
TeamID: "team_id_1",
|
||||
Type: "custom",
|
||||
}, nil)
|
||||
|
||||
deletedCategory, err := th.App.DeleteCategory("category_id_1", "user_id_1", "team_id_1")
|
||||
assert.NotNil(t, deletedCategory)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("trying to delete system category", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetCategory("category_id_1").Return(&model.Category{
|
||||
ID: "category_id_1",
|
||||
DeleteAt: 0,
|
||||
UserID: "user_id_1",
|
||||
TeamID: "team_id_1",
|
||||
Type: "system",
|
||||
}, nil)
|
||||
|
||||
deletedCategory, err := th.App.DeleteCategory("category_id_1", "user_id_1", "team_id_1")
|
||||
assert.Nil(t, deletedCategory)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
|
@ -4,6 +4,8 @@ import (
|
|||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -47,6 +49,14 @@ func TestApp_ImportArchive(t *testing.T) {
|
|||
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")
|
||||
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil)
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "boards_category_id",
|
||||
Name: "Boards",
|
||||
}, nil)
|
||||
th.Store.EXPECT().GetBoardsForUserAndTeam("user", "test-team", false).Return([]*model.Board{}, nil)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user", "boards_category_id", utils.Anything).Return(nil)
|
||||
|
||||
err := th.App.ImportArchive(r, opts)
|
||||
require.NoError(t, err, "import archive should not fail")
|
||||
|
|
|
@ -3,6 +3,8 @@ package app
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -26,10 +28,19 @@ func TestPrepareOnboardingTour(t *testing.T) {
|
|||
}
|
||||
|
||||
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil)
|
||||
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}},
|
||||
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).Return(&model.BoardsAndBlocks{Boards: []*model.Board{
|
||||
{
|
||||
ID: "board_id_2",
|
||||
Title: "Welcome to Boards!",
|
||||
TeamID: "0",
|
||||
IsTemplate: true,
|
||||
},
|
||||
}},
|
||||
nil, nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil).Times(3)
|
||||
th.Store.EXPECT().GetBoard(welcomeBoard.ID).Return(&welcomeBoard, nil).AnyTimes()
|
||||
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil).Times(2)
|
||||
th.Store.EXPECT().GetMembersForBoard("board_id_2").Return([]*model.BoardMember{}, nil).Times(1)
|
||||
th.Store.EXPECT().GetBoard(welcomeBoard.ID).Return(&welcomeBoard, nil).Times(1)
|
||||
th.Store.EXPECT().GetBoard("board_id_2").Return(&welcomeBoard, nil).Times(1)
|
||||
th.Store.EXPECT().GetUsersByTeam("0", "").Return([]*model.User{}, nil)
|
||||
|
||||
privateWelcomeBoard := model.Board{
|
||||
|
@ -40,7 +51,7 @@ func TestPrepareOnboardingTour(t *testing.T) {
|
|||
Type: model.BoardTypePrivate,
|
||||
}
|
||||
newType := model.BoardTypePrivate
|
||||
th.Store.EXPECT().PatchBoard("board_id_1", &model.BoardPatch{Type: &newType}, "user_id_1").Return(&privateWelcomeBoard, nil)
|
||||
th.Store.EXPECT().PatchBoard("board_id_2", &model.BoardPatch{Type: &newType}, "user_id_1").Return(&privateWelcomeBoard, nil)
|
||||
|
||||
userPreferencesPatch := model.UserPreferencesPatch{
|
||||
UpdatedFields: map[string]string{
|
||||
|
@ -51,7 +62,22 @@ func TestPrepareOnboardingTour(t *testing.T) {
|
|||
}
|
||||
|
||||
th.Store.EXPECT().PatchUserPreferences(userID, userPreferencesPatch).Return(nil, nil)
|
||||
th.Store.EXPECT().GetUserCategoryBoards(userID, "0").Return([]model.CategoryBoards{}, nil)
|
||||
th.Store.EXPECT().GetUserCategoryBoards(userID, "team_id").Return([]model.CategoryBoards{}, nil).Times(1)
|
||||
|
||||
// when this is called the second time, the default category is created so we need to include that in the response list
|
||||
th.Store.EXPECT().GetUserCategoryBoards(userID, "team_id").Return([]model.CategoryBoards{
|
||||
{
|
||||
Category: model.Category{ID: "boards_category_id", Name: "Boards"},
|
||||
},
|
||||
}, nil).Times(1)
|
||||
|
||||
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil).Times(1)
|
||||
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
|
||||
ID: "boards_category",
|
||||
Name: "Boards",
|
||||
}, nil)
|
||||
th.Store.EXPECT().GetBoardsForUserAndTeam("user_id_1", teamID, false).Return([]*model.Board{}, nil)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "boards_category_id", "board_id_2").Return(nil)
|
||||
|
||||
teamID, boardID, err := th.App.PrepareOnboardingTour(userID, teamID)
|
||||
assert.NoError(t, err)
|
||||
|
@ -89,7 +115,12 @@ func TestCreateWelcomeBoard(t *testing.T) {
|
|||
}
|
||||
newType := model.BoardTypePrivate
|
||||
th.Store.EXPECT().PatchBoard("board_id_1", &model.BoardPatch{Type: &newType}, "user_id_1").Return(&privateWelcomeBoard, nil)
|
||||
th.Store.EXPECT().GetUserCategoryBoards(userID, "0")
|
||||
th.Store.EXPECT().GetUserCategoryBoards(userID, "team_id").Return([]model.CategoryBoards{
|
||||
{
|
||||
Category: model.Category{ID: "boards_category_id", Name: "Boards"},
|
||||
},
|
||||
}, nil).Times(2)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "boards_category_id", "board_id_1").Return(nil)
|
||||
|
||||
boardID, err := th.App.createWelcomeBoard(userID, teamID)
|
||||
assert.Nil(t, err)
|
||||
|
|
|
@ -729,7 +729,8 @@ func TestDeleteBoardsAndBlocks(t *testing.T) {
|
|||
|
||||
// the user is an admin of the first board
|
||||
newBoard1 := &model.Board{
|
||||
Type: model.BoardTypeOpen,
|
||||
Type: model.BoardTypeOpen,
|
||||
TeamID: "team_id_1",
|
||||
}
|
||||
board1, err := th.Server.App().CreateBoard(newBoard1, th.GetUser1().ID, true)
|
||||
require.NoError(t, err)
|
||||
|
@ -737,7 +738,8 @@ func TestDeleteBoardsAndBlocks(t *testing.T) {
|
|||
|
||||
// but not of the second
|
||||
newBoard2 := &model.Board{
|
||||
Type: model.BoardTypeOpen,
|
||||
Type: model.BoardTypeOpen,
|
||||
TeamID: "team_id_1",
|
||||
}
|
||||
board2, err := th.Server.App().CreateBoard(newBoard2, th.GetUser1().ID, false)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -43,6 +43,18 @@ type TestCase struct {
|
|||
totalResults int
|
||||
}
|
||||
|
||||
func (tt TestCase) identifier() string {
|
||||
return fmt.Sprintf(
|
||||
"url: %s method: %s body: %s userRoles: %s expectedStatusCode: %d totalResults: %d",
|
||||
tt.url,
|
||||
tt.method,
|
||||
tt.body,
|
||||
tt.userRole,
|
||||
tt.expectedStatusCode,
|
||||
tt.totalResults,
|
||||
)
|
||||
}
|
||||
|
||||
func setupClients(th *TestHelper) Clients {
|
||||
// user1
|
||||
clients := Clients{
|
||||
|
@ -262,7 +274,6 @@ func runTestCases(t *testing.T, ttCases []TestCase, testData TestData, clients C
|
|||
url = strings.ReplaceAll(url, "{PUBLIC_BOARD_ID}", testData.publicBoard.ID)
|
||||
url = strings.ReplaceAll(url, "{PUBLIC_TEMPLATE_ID}", testData.publicTemplate.ID)
|
||||
url = strings.ReplaceAll(url, "{PRIVATE_TEMPLATE_ID}", testData.privateTemplate.ID)
|
||||
|
||||
url = strings.ReplaceAll(url, "{USER_ANON_ID}", userAnonID)
|
||||
url = strings.ReplaceAll(url, "{USER_NO_TEAM_MEMBER_ID}", userNoTeamMemberID)
|
||||
url = strings.ReplaceAll(url, "{USER_TEAM_MEMBER_ID}", userTeamMemberID)
|
||||
|
@ -273,7 +284,7 @@ func runTestCases(t *testing.T, ttCases []TestCase, testData TestData, clients C
|
|||
url = strings.ReplaceAll(url, "{USER_GUEST_ID}", userGuestID)
|
||||
|
||||
if strings.Contains(url, "{") || strings.Contains(url, "}") {
|
||||
require.Fail(t, "Unreplaced tokens in url", url)
|
||||
require.Fail(t, "Unreplaced tokens in url", url, tc.identifier())
|
||||
}
|
||||
|
||||
var response *http.Response
|
||||
|
@ -296,28 +307,28 @@ func runTestCases(t *testing.T, ttCases []TestCase, testData TestData, clients C
|
|||
defer response.Body.Close()
|
||||
}
|
||||
|
||||
require.Equal(t, tc.expectedStatusCode, response.StatusCode)
|
||||
require.Equal(t, tc.expectedStatusCode, response.StatusCode, tc.identifier())
|
||||
if tc.expectedStatusCode >= 200 && tc.expectedStatusCode < 300 {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err, tc.identifier())
|
||||
}
|
||||
if tc.expectedStatusCode >= 200 && tc.expectedStatusCode < 300 {
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
require.Fail(t, err.Error())
|
||||
require.Fail(t, err.Error(), tc.identifier())
|
||||
}
|
||||
if strings.HasPrefix(string(body), "[") {
|
||||
var data []interface{}
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
require.Fail(t, err.Error())
|
||||
require.Fail(t, err.Error(), tc.identifier())
|
||||
}
|
||||
require.Len(t, data, tc.totalResults)
|
||||
require.Len(t, data, tc.totalResults, tc.identifier())
|
||||
} else {
|
||||
if tc.totalResults > 0 {
|
||||
require.Equal(t, 1, tc.totalResults)
|
||||
require.Greater(t, len(string(body)), 2)
|
||||
require.Greater(t, len(string(body)), 2, tc.identifier())
|
||||
} else {
|
||||
require.Len(t, string(body), 2)
|
||||
require.Len(t, string(body), 2, tc.identifier())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2865,14 +2876,14 @@ func TestPermissionsClientConfig(t *testing.T) {
|
|||
|
||||
func TestPermissionsGetCategories(t *testing.T) {
|
||||
ttCases := []TestCase{
|
||||
{"/teams/test-team/categories", methodGet, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/teams/test-team/categories", methodGet, "", userNoTeamMember, http.StatusOK, 0},
|
||||
{"/teams/test-team/categories", methodGet, "", userTeamMember, http.StatusOK, 0},
|
||||
{"/teams/test-team/categories", methodGet, "", userViewer, http.StatusOK, 0},
|
||||
{"/teams/test-team/categories", methodGet, "", userCommenter, http.StatusOK, 0},
|
||||
{"/teams/test-team/categories", methodGet, "", userEditor, http.StatusOK, 0},
|
||||
{"/teams/test-team/categories", methodGet, "", userAdmin, http.StatusOK, 0},
|
||||
{"/teams/test-team/categories", methodGet, "", userGuest, http.StatusOK, 0},
|
||||
{"/teams/test-team/categories", methodGet, "", userAnon, http.StatusUnauthorized, 1},
|
||||
{"/teams/test-team/categories", methodGet, "", userNoTeamMember, http.StatusOK, 1},
|
||||
{"/teams/test-team/categories", methodGet, "", userTeamMember, http.StatusOK, 1},
|
||||
{"/teams/test-team/categories", methodGet, "", userViewer, http.StatusOK, 1},
|
||||
{"/teams/test-team/categories", methodGet, "", userCommenter, http.StatusOK, 1},
|
||||
{"/teams/test-team/categories", methodGet, "", userEditor, http.StatusOK, 1},
|
||||
{"/teams/test-team/categories", methodGet, "", userAdmin, http.StatusOK, 1},
|
||||
{"/teams/test-team/categories", methodGet, "", userGuest, http.StatusOK, 1},
|
||||
}
|
||||
|
||||
t.Run("plugin", func(t *testing.T) {
|
||||
|
@ -2960,6 +2971,7 @@ func TestPermissionsUpdateCategory(t *testing.T) {
|
|||
UserID: userID,
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
Type: "custom",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,18 @@ package model
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
CategoryTypeSystem = "system"
|
||||
CategoryTypeCustom = "custom"
|
||||
)
|
||||
|
||||
// Category is a board category
|
||||
// swagger:model
|
||||
type Category struct {
|
||||
|
@ -42,12 +48,19 @@ type Category struct {
|
|||
// Category's state in client side
|
||||
// required: true
|
||||
Collapsed bool `json:"collapsed"`
|
||||
|
||||
// Category's type
|
||||
// required: true
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (c *Category) Hydrate() {
|
||||
c.ID = utils.NewID(utils.IDTypeNone)
|
||||
c.CreateAt = utils.GetMillis()
|
||||
c.UpdateAt = c.CreateAt
|
||||
if c.Type == "" {
|
||||
c.Type = CategoryTypeCustom
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Category) IsValid() error {
|
||||
|
@ -67,6 +80,10 @@ func (c *Category) IsValid() error {
|
|||
return NewErrInvalidCategory("category team id ID cannot be empty")
|
||||
}
|
||||
|
||||
if c.Type != CategoryTypeCustom && c.Type != CategoryTypeSystem {
|
||||
return NewErrInvalidCategory(fmt.Sprintf("category type is invalid. Allowed types: %s and %s", CategoryTypeSystem, CategoryTypeCustom))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
func (s *SQLStore) getCategory(db sq.BaseRunner, id string) (*model.Category, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select("id", "name", "user_id", "team_id", "create_at", "update_at", "delete_at", "collapsed").
|
||||
Select("id", "name", "user_id", "team_id", "create_at", "update_at", "delete_at", "collapsed", "type").
|
||||
From(s.tablePrefix + "categories").
|
||||
Where(sq.Eq{"id": id})
|
||||
|
||||
|
@ -47,6 +47,7 @@ func (s *SQLStore) createCategory(db sq.BaseRunner, category model.Category) err
|
|||
"update_at",
|
||||
"delete_at",
|
||||
"collapsed",
|
||||
"type",
|
||||
).
|
||||
Values(
|
||||
category.ID,
|
||||
|
@ -57,6 +58,7 @@ func (s *SQLStore) createCategory(db sq.BaseRunner, category model.Category) err
|
|||
category.UpdateAt,
|
||||
category.DeleteAt,
|
||||
category.Collapsed,
|
||||
category.Type,
|
||||
)
|
||||
|
||||
_, err := query.Exec()
|
||||
|
@ -73,7 +75,10 @@ func (s *SQLStore) updateCategory(db sq.BaseRunner, category model.Category) err
|
|||
Set("name", category.Name).
|
||||
Set("update_at", category.UpdateAt).
|
||||
Set("collapsed", category.Collapsed).
|
||||
Where(sq.Eq{"id": category.ID})
|
||||
Where(sq.Eq{
|
||||
"id": category.ID,
|
||||
"delete_at": 0,
|
||||
})
|
||||
|
||||
_, err := query.Exec()
|
||||
if err != nil {
|
||||
|
@ -88,9 +93,10 @@ func (s *SQLStore) deleteCategory(db sq.BaseRunner, categoryID, userID, teamID s
|
|||
Update(s.tablePrefix+"categories").
|
||||
Set("delete_at", utils.GetMillis()).
|
||||
Where(sq.Eq{
|
||||
"id": categoryID,
|
||||
"user_id": userID,
|
||||
"team_id": teamID,
|
||||
"id": categoryID,
|
||||
"user_id": userID,
|
||||
"team_id": teamID,
|
||||
"delete_at": 0,
|
||||
})
|
||||
|
||||
_, err := query.Exec()
|
||||
|
@ -109,7 +115,7 @@ func (s *SQLStore) deleteCategory(db sq.BaseRunner, categoryID, userID, teamID s
|
|||
|
||||
func (s *SQLStore) getUserCategories(db sq.BaseRunner, userID, teamID string) ([]model.Category, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select("id", "name", "user_id", "team_id", "create_at", "update_at", "delete_at", "collapsed").
|
||||
Select("id", "name", "user_id", "team_id", "create_at", "update_at", "delete_at", "collapsed", "type").
|
||||
From(s.tablePrefix + "categories").
|
||||
Where(sq.Eq{
|
||||
"user_id": userID,
|
||||
|
@ -140,6 +146,7 @@ func (s *SQLStore) categoriesFromRows(rows *sql.Rows) ([]model.Category, error)
|
|||
&category.UpdateAt,
|
||||
&category.DeleteAt,
|
||||
&category.Collapsed,
|
||||
&category.Type,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE {{.prefix}}categories DROP COLUMN type;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {{.prefix}}categories ADD COLUMN type varchar(64);
|
||||
UPDATE {{.prefix}}categories SET type = 'custom' WHERE type IS NULL;
|
5
server/utils/testUtils.go
Normal file
5
server/utils/testUtils.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package utils
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
var Anything = mock.MatchedBy(func(interface{}) bool { return true })
|
|
@ -133,6 +133,9 @@ describe('Create and delete board / card', () => {
|
|||
|
||||
// Delete board
|
||||
cy.log('**Delete board**')
|
||||
cy.get('.Sidebar .octo-sidebar-list').then((el) => {
|
||||
cy.log(el.text())
|
||||
})
|
||||
cy.get('.Sidebar .octo-sidebar-list').
|
||||
contains(boardTitle).
|
||||
parent().
|
||||
|
|
|
@ -207,48 +207,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarCategory"
|
||||
>
|
||||
<div
|
||||
class="octo-sidebar-item category ' expanded "
|
||||
>
|
||||
<div
|
||||
class="octo-sidebar-title category-title"
|
||||
title="Boards"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down ChevronDownIcon"
|
||||
/>
|
||||
Boards
|
||||
<div
|
||||
class="sidebarCategoriesTour"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-sidebar-item subitem no-views"
|
||||
>
|
||||
No boards inside
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
|
@ -1348,48 +1306,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarCategory"
|
||||
>
|
||||
<div
|
||||
class="octo-sidebar-item category ' expanded "
|
||||
>
|
||||
<div
|
||||
class="octo-sidebar-title category-title"
|
||||
title="Boards"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down ChevronDownIcon"
|
||||
/>
|
||||
Boards
|
||||
<div
|
||||
class="sidebarCategoriesTour"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-sidebar-item subitem no-views"
|
||||
>
|
||||
No boards inside
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
|
|
|
@ -152,49 +152,6 @@ exports[`components/sidebarSidebar dont show hidden boards 1`] = `
|
|||
No boards inside
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarCategory"
|
||||
>
|
||||
<div
|
||||
class="octo-sidebar-item category ' expanded "
|
||||
>
|
||||
<div
|
||||
class="octo-sidebar-title category-title"
|
||||
title="Boards"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down ChevronDownIcon"
|
||||
/>
|
||||
Boards
|
||||
<div
|
||||
class="sidebarCategoriesTour"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-sidebar-item subitem no-views"
|
||||
>
|
||||
No boards inside
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
|
@ -459,49 +416,6 @@ exports[`components/sidebarSidebar sidebar hidden 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarCategory"
|
||||
>
|
||||
<div
|
||||
class="octo-sidebar-item category ' expanded "
|
||||
>
|
||||
<div
|
||||
class="octo-sidebar-title category-title"
|
||||
title="Boards"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down ChevronDownIcon"
|
||||
/>
|
||||
Boards
|
||||
<div
|
||||
class="sidebarCategoriesTour"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-sidebar-item subitem no-views"
|
||||
>
|
||||
No boards inside
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
|
@ -804,49 +718,6 @@ exports[`components/sidebarSidebar some categories hidden 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarCategory"
|
||||
>
|
||||
<div
|
||||
class="octo-sidebar-item category ' expanded "
|
||||
>
|
||||
<div
|
||||
class="octo-sidebar-title category-title"
|
||||
title="Boards"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down ChevronDownIcon"
|
||||
/>
|
||||
Boards
|
||||
<div
|
||||
class="sidebarCategoriesTour"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-sidebar-item subitem no-views"
|
||||
>
|
||||
No boards inside
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
|
|
|
@ -213,7 +213,7 @@ describe('components/sidebarSidebar', () => {
|
|||
expect(sidebarBoards.length).toBe(0)
|
||||
|
||||
const noBoardsText = getAllByText('No boards inside')
|
||||
expect(noBoardsText.length).toBe(2) // one for custom category, one for default category
|
||||
expect(noBoardsText.length).toBe(1)
|
||||
})
|
||||
|
||||
test('some categories hidden', () => {
|
||||
|
|
|
@ -38,7 +38,6 @@ import {getCurrentViewId} from '../../store/views'
|
|||
import SidebarCategory from './sidebarCategory'
|
||||
import SidebarSettingsMenu from './sidebarSettingsMenu'
|
||||
import SidebarUserMenu from './sidebarUserMenu'
|
||||
import {addMissingItems} from './utils'
|
||||
|
||||
type Props = {
|
||||
activeBoardId?: string
|
||||
|
@ -60,9 +59,8 @@ const Sidebar = (props: Props) => {
|
|||
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
|
||||
const boards = useAppSelector(getMySortedBoards)
|
||||
const dispatch = useAppDispatch()
|
||||
const partialCategories = useAppSelector<CategoryBoards[]>(getSidebarCategories)
|
||||
const sidebarCategories = useAppSelector<CategoryBoards[]>(getSidebarCategories)
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const sidebarCategories = addMissingItems(partialCategories, boards)
|
||||
const activeViewID = useAppSelector(getCurrentViewId)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -101,6 +99,8 @@ const Sidebar = (props: Props) => {
|
|||
}, [windowDimensions])
|
||||
|
||||
if (!boards) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('AAAA')
|
||||
return <div/>
|
||||
}
|
||||
|
||||
|
@ -115,10 +115,14 @@ const Sidebar = (props: Props) => {
|
|||
}
|
||||
|
||||
if (!me) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('BBBB')
|
||||
return <div/>
|
||||
}
|
||||
|
||||
if (isHidden) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('CCCC')
|
||||
return (
|
||||
<div className='Sidebar octo-sidebar hidden'>
|
||||
<div className='octo-sidebar-header show-button'>
|
||||
|
@ -145,6 +149,8 @@ const Sidebar = (props: Props) => {
|
|||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('DDDD')
|
||||
return (
|
||||
<div className='Sidebar octo-sidebar'>
|
||||
{!Utils.isFocalboardPlugin() &&
|
||||
|
|
|
@ -246,14 +246,8 @@ const SidebarCategory = (props: Props) => {
|
|||
position='auto'
|
||||
parentRef={menuWrapperRef}
|
||||
>
|
||||
<Menu.Text
|
||||
id='createNewCategory'
|
||||
name={intl.formatMessage({id: 'SidebarCategories.CategoryMenu.CreateNew', defaultMessage: 'Create New Category'})}
|
||||
icon={<CreateNewFolder/>}
|
||||
onClick={handleCreateNewCategory}
|
||||
/>
|
||||
{
|
||||
props.categoryBoards.id !== '' &&
|
||||
props.categoryBoards.type === 'custom' &&
|
||||
<React.Fragment>
|
||||
<Menu.Text
|
||||
id='updateCategory'
|
||||
|
@ -269,14 +263,14 @@ const SidebarCategory = (props: Props) => {
|
|||
onClick={() => setShowDeleteCategoryDialog(true)}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
<Menu.Text
|
||||
id='createNewCategory'
|
||||
name={intl.formatMessage({id: 'SidebarCategories.CategoryMenu.CreateNew', defaultMessage: 'Create New Category'})}
|
||||
icon={<CreateNewFolder/>}
|
||||
onClick={handleCreateNewCategory}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
<Menu.Text
|
||||
id='createNewCategory'
|
||||
name={intl.formatMessage({id: 'SidebarCategories.CategoryMenu.CreateNew', defaultMessage: 'Create New Category'})}
|
||||
icon={<CreateNewFolder/>}
|
||||
onClick={handleCreateNewCategory}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
|
@ -305,7 +299,7 @@ const SidebarCategory = (props: Props) => {
|
|||
/>
|
||||
)
|
||||
})}
|
||||
{!collapsed && props.boards.map((board: Board) => {
|
||||
{!collapsed && props.boards.filter((board) => !board.isTemplate).map((board: Board) => {
|
||||
if (!isBoardVisible(board.id)) {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {CategoryBoards, DefaultCategory} from '../../store/sidebar'
|
||||
|
||||
import {Block} from '../../blocks/block'
|
||||
import {Board} from '../../blocks/board'
|
||||
|
||||
export function addMissingItems(sidebarCategories: CategoryBoards[], allItems: Array<Block | Board>): CategoryBoards[] {
|
||||
const blocksInCategories = new Map<string, boolean>()
|
||||
sidebarCategories.forEach(
|
||||
(category) => category.boardIDs.forEach(
|
||||
(boardID) => blocksInCategories.set(boardID, true),
|
||||
),
|
||||
)
|
||||
|
||||
const defaultCategory: CategoryBoards = {
|
||||
...DefaultCategory,
|
||||
boardIDs: [],
|
||||
}
|
||||
|
||||
allItems.forEach((block) => {
|
||||
if (!blocksInCategories.get(block.id)) {
|
||||
defaultCategory.boardIDs.push(block.id)
|
||||
}
|
||||
})
|
||||
|
||||
// sidebarCategories comes from store,
|
||||
// so is frozen and can't be extended.
|
||||
// So creating new array from it.
|
||||
return [
|
||||
...sidebarCategories,
|
||||
defaultCategory,
|
||||
]
|
||||
}
|
|
@ -7,6 +7,8 @@ import {default as client} from '../octoClient'
|
|||
|
||||
import {RootState} from './index'
|
||||
|
||||
export type CategoryType = 'system' | 'custom'
|
||||
|
||||
interface Category {
|
||||
id: string
|
||||
name: string
|
||||
|
@ -16,6 +18,7 @@ interface Category {
|
|||
updateAt: number
|
||||
deleteAt: number
|
||||
collapsed: boolean
|
||||
type: CategoryType
|
||||
}
|
||||
|
||||
interface CategoryBoards extends Category {
|
||||
|
|
|
@ -173,6 +173,7 @@ class TestBlockFactory {
|
|||
userID: '',
|
||||
teamID: '',
|
||||
collapsed: false,
|
||||
type: 'custom',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue