You've already forked focalboard
							
							
				mirror of
				https://github.com/mattermost/focalboard.git
				synced 2025-10-31 00:17:42 +02:00 
			
		
		
		
	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:
		| @@ -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', | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user