Duplicate board retain category (#3395)

* Converted synthetic membership to natuaral if needed

* Creating duplicated board to same category as parent board

* fixed tests

* fixed an incorrect API path in client

* Added logic to prevent jumping of new board between categories

* fixed lint error
This commit is contained in:
Harshil Sharma 2022-08-10 16:31:34 +05:30 committed by GitHub
parent 75036d5618
commit ab02e98831
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 201 additions and 1 deletions

View file

@ -145,6 +145,36 @@ func (a *App) getBoardDescendantModifiedInfo(boardID string, latest bool) (int64
return timestamp, modifiedBy, nil
}
func (a *App) setBoardCategoryFromSource(sourceBoardID, destinationBoardID, userID, teamID string) error {
// find source board's category ID for the user
userCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID)
if err != nil {
return err
}
var destinationCategoryID string
for _, categoryBoard := range userCategoryBoards {
for _, boardID := range categoryBoard.BoardIDs {
if boardID == sourceBoardID {
// category found!
destinationCategoryID = categoryBoard.ID
break
}
}
}
// if source board is not mapped to a category for this user,
// then we have nothing more to do.
if destinationCategoryID == "" {
return nil
}
// now that we have source board's category,
// we send destination board to the same category
return a.AddUpdateUserCategoryBoard(teamID, userID, destinationCategoryID, destinationBoardID)
}
func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
bab, members, err := a.store.DuplicateBoard(boardID, userID, toTeam, asTemplate)
if err != nil {
@ -156,6 +186,12 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
a.logger.Error("Could not copy files while duplicating board", mlog.String("BoardID", boardID), mlog.Err(err))
}
for _, board := range bab.Boards {
if categoryErr := a.setBoardCategoryFromSource(boardID, board.ID, userID, board.TeamID); categoryErr != nil {
return nil, nil, categoryErr
}
}
// bab.Blocks now has updated file ids for any blocks containing files. We need to store them.
blockIDs := make([]string, 0)
blockPatches := make([]model.BlockPatch, 0)

View file

@ -50,6 +50,7 @@ func TestPrepareOnboardingTour(t *testing.T) {
}
th.Store.EXPECT().PatchUserProps(userID, userPropPatch).Return(nil)
th.Store.EXPECT().GetUserCategoryBoards(userID, "0").Return([]model.CategoryBoards{}, nil)
teamID, boardID, err := th.App.PrepareOnboardingTour(userID, teamID)
assert.NoError(t, err)
@ -86,6 +87,7 @@ 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")
boardID, err := th.App.createWelcomeBoard(userID, teamID)
assert.Nil(t, err)

View file

@ -348,6 +348,38 @@ func (c *Client) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks) (*model.Board
return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) CreateCategory(category model.Category) (*model.Category, *Response) {
r, err := c.DoAPIPost(c.GetTeamRoute(category.TeamID)+"/categories", toJSON(category))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.CategoryFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) UpdateCategoryBoard(teamID, categoryID, boardID string) *Response {
r, err := c.DoAPIPost(fmt.Sprintf("%s/categories/%s/boards/%s", c.GetTeamRoute(teamID), categoryID, boardID), "")
if err != nil {
return BuildErrorResponse(r, err)
}
defer closeBody(r)
return BuildResponse(r)
}
func (c *Client) GetUserCategoryBoards(teamID string) ([]model.CategoryBoards, *Response) {
r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/categories", "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
var categoryBoards []model.CategoryBoards
_ = json.NewDecoder(r.Body).Decode(&categoryBoards)
return categoryBoards, BuildResponse(r)
}
func (c *Client) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks) (*model.BoardsAndBlocks, *Response) {
r, err := c.DoAPIPatch(c.GetBoardsAndBlocksRoute(), toJSON(pbab))
if err != nil {

View file

@ -1936,6 +1936,108 @@ func TestDuplicateBoard(t *testing.T) {
require.Equal(t, duplicateBoard.ID, members[0].BoardID)
require.True(t, members[0].SchemeAdmin)
})
t.Run("create and duplicate public board from a custom category", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
me := th.GetUser1()
teamID := testTeamID
category := model.Category{
Name: "My Category",
UserID: me.ID,
TeamID: teamID,
}
createdCategory, resp := th.Client.CreateCategory(category)
th.CheckOK(resp)
require.NoError(t, resp.Error)
require.NotNil(t, createdCategory)
require.Equal(t, "My Category", createdCategory.Name)
require.Equal(t, me.ID, createdCategory.UserID)
require.Equal(t, teamID, createdCategory.TeamID)
title := "Public board"
newBoard := &model.Board{
Title: title,
Type: model.BoardTypeOpen,
TeamID: teamID,
}
board, resp := th.Client.CreateBoard(newBoard)
th.CheckOK(resp)
require.NoError(t, resp.Error)
require.NotNil(t, board)
require.NotNil(t, board.ID)
require.Equal(t, title, board.Title)
require.Equal(t, model.BoardTypeOpen, board.Type)
require.Equal(t, teamID, board.TeamID)
require.Equal(t, me.ID, board.CreatedBy)
require.Equal(t, me.ID, board.ModifiedBy)
// move board to custom category
resp = th.Client.UpdateCategoryBoard(teamID, createdCategory.ID, board.ID)
th.CheckOK(resp)
require.NoError(t, resp.Error)
newBlocks := []model.Block{
{
ID: utils.NewID(utils.IDTypeBlock),
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Title: "View 1",
Type: model.TypeView,
},
}
newBlocks, resp = th.Client.InsertBlocks(board.ID, newBlocks)
require.NoError(t, resp.Error)
require.Len(t, newBlocks, 1)
newUserMember := &model.BoardMember{
UserID: th.GetUser2().ID,
BoardID: board.ID,
SchemeEditor: true,
}
th.Client.AddMemberToBoard(newUserMember)
members, err := th.Server.App().GetMembersForBoard(board.ID)
require.NoError(t, err)
require.Len(t, members, 2)
// Duplicate the board
rBoardsAndBlock, resp := th.Client.DuplicateBoard(board.ID, false, teamID)
th.CheckOK(resp)
require.NotNil(t, rBoardsAndBlock)
require.Equal(t, len(rBoardsAndBlock.Boards), 1)
require.Equal(t, len(rBoardsAndBlock.Blocks), 1)
duplicateBoard := rBoardsAndBlock.Boards[0]
require.Equal(t, duplicateBoard.Type, model.BoardTypePrivate, "Duplicated board should be private")
require.Equal(t, "Public board copy", duplicateBoard.Title)
members, err = th.Server.App().GetMembersForBoard(duplicateBoard.ID)
require.NoError(t, err)
require.Len(t, members, 1, "Duplicated board should only have one member")
require.Equal(t, me.ID, members[0].UserID)
require.Equal(t, duplicateBoard.ID, members[0].BoardID)
require.True(t, members[0].SchemeAdmin)
// verify duplicated board is in the same custom category
userCategoryBoards, resp := th.Client.GetUserCategoryBoards(teamID)
th.CheckOK(resp)
require.NotNil(t, rBoardsAndBlock)
var duplicateBoardCategoryID string
for _, categoryBoard := range userCategoryBoards {
for _, boardID := range categoryBoard.BoardIDs {
if boardID == duplicateBoard.ID {
duplicateBoardCategoryID = categoryBoard.Category.ID
}
}
}
require.Equal(t, createdCategory.ID, duplicateBoardCategoryID)
})
}
func TestJoinBoard(t *testing.T) {

View file

@ -1,6 +1,8 @@
package model
import (
"encoding/json"
"io"
"strings"
"github.com/mattermost/focalboard/server/utils"
@ -81,3 +83,9 @@ func newErrInvalidCategory(msg string) *ErrInvalidCategory {
func (e *ErrInvalidCategory) Error() string {
return e.msg
}
func CategoryFromJSON(data io.Reader) *Category {
var category *Category
_ = json.NewDecoder(data).Decode(&category)
return category
}

View file

@ -15,7 +15,7 @@ import MenuWrapper from '../../widgets/menuWrapper'
import BoardPermissionGate from '../permissions/boardPermissionGate'
import './sidebarBoardItem.scss'
import {CategoryBoards} from '../../store/sidebar'
import {CategoryBoards, updateBoardCategories} from '../../store/sidebar'
import CreateNewFolder from '../../widgets/icons/newFolder'
import {useAppDispatch, useAppSelector} from '../../store/hooks'
import {getCurrentBoardViews, getCurrentViewId} from '../../store/views'
@ -113,6 +113,26 @@ const SidebarBoardItem = (props: Props) => {
}
const boardId = blocksAndBoards.boards[0].id
// If the source board is in a custom category, set the new board in
// the same category. Even though the server does this as well on its side,
// we need to do this to avoid the duplicated board showing up in default "Boards" category first
// then jumping to the custom category.
// The jump would happen because when server clones a board from a custom category,
// two WS events are sent - first to indicate the new board belongs to the specific category,
// second, to indicate the new board is created. Depending on the order of execution of the two
// WS event handlers, if the handler for second events executes first, it will show the new board
// in default category in LHS, then when the handler for first events gets executed, it moves the board
// to the correct category.
// By not waiting for the board-category WS event and setting the right category for the board,
// we avoid the jumping behavior.
if (props.categoryBoards.id !== '') {
await dispatch(updateBoardCategories([{
boardID: boardId,
categoryID: props.categoryBoards.id,
}]))
}
Utils.showBoard(boardId, match, history)
}, [board.id])