mirror of
https://github.com/mattermost/focalboard.git
synced 2025-03-20 20:45:00 +02:00
Adding the permissions for commenter + viewer roles (#2882)
Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Paul Esch-Laurent <paul.esch-laurent@mattermost.com>
This commit is contained in:
parent
03a6a963eb
commit
2b39745f68
@ -221,13 +221,6 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
val := r.URL.Query().Get("disable_notify")
|
||||
disableNotify := val == True
|
||||
|
||||
// in phase 1 we use "manage_board_cards", but we would have to
|
||||
// check on specific actions for phase 2
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||
return
|
||||
}
|
||||
|
||||
requestBody, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -242,6 +235,8 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
hasComments := false
|
||||
hasContents := false
|
||||
for _, block := range blocks {
|
||||
// Error checking
|
||||
if len(block.Type) < 1 {
|
||||
@ -250,6 +245,12 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if block.Type == model.TypeComment {
|
||||
hasComments = true
|
||||
} else {
|
||||
hasContents = true
|
||||
}
|
||||
|
||||
if block.CreateAt < 1 {
|
||||
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
||||
@ -269,6 +270,19 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if hasContents {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if hasComments {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to post card comments"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
blocks = model.GenerateBlockIDs(blocks, a.logger)
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
|
||||
@ -748,9 +762,16 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||
return
|
||||
if block.Type == model.TypeComment {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to comment on board cards"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board cards"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail)
|
||||
|
@ -1075,6 +1075,74 @@ func TestPermissionsCreateBoardBlocks(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionsCreateBoardComments(t *testing.T) {
|
||||
ttCasesF := func(testData TestData) []TestCase {
|
||||
counter := 0
|
||||
newBlockJSON := func(boardID string) string {
|
||||
counter++
|
||||
return toJSON(t, []*model.Block{{
|
||||
ID: fmt.Sprintf("%d", counter),
|
||||
Title: "Comment to create",
|
||||
BoardID: boardID,
|
||||
Type: model.TypeComment,
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
}})
|
||||
}
|
||||
|
||||
return []TestCase{
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userCommenter, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAdmin, http.StatusOK, 1},
|
||||
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userCommenter, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAdmin, http.StatusOK, 1},
|
||||
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userCommenter, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAdmin, http.StatusOK, 1},
|
||||
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userCommenter, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAdmin, http.StatusOK, 1},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("plugin", func(t *testing.T) {
|
||||
th := SetupTestHelperPluginMode(t)
|
||||
defer th.TearDown()
|
||||
clients := setupClients(th)
|
||||
testData := setupData(t, th)
|
||||
ttCases := ttCasesF(testData)
|
||||
runTestCases(t, ttCases, testData, clients)
|
||||
})
|
||||
t.Run("local", func(t *testing.T) {
|
||||
th := SetupTestHelperLocalMode(t)
|
||||
defer th.TearDown()
|
||||
clients := setupLocalClients(th)
|
||||
testData := setupData(t, th)
|
||||
ttCases := ttCasesF(testData)
|
||||
runTestCases(t, ttCases, testData, clients)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionsPatchBoardBlocks(t *testing.T) {
|
||||
newBlocksPatchJSON := func(blockID string) string {
|
||||
newTitle := "New Patch Block Title"
|
||||
@ -1420,37 +1488,104 @@ func TestPermissionsDuplicateBoardBlock(t *testing.T) {
|
||||
}
|
||||
|
||||
ttCases := []TestCase{
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
|
||||
// Invalid boardID/blockID combination
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusNotFound, 0},
|
||||
}
|
||||
|
||||
t.Run("plugin", func(t *testing.T) {
|
||||
th := SetupTestHelperPluginMode(t)
|
||||
defer th.TearDown()
|
||||
clients := setupClients(th)
|
||||
testData := setupData(t, th)
|
||||
extraSetup(t, th, testData)
|
||||
runTestCases(t, ttCases, testData, clients)
|
||||
})
|
||||
t.Run("local", func(t *testing.T) {
|
||||
th := SetupTestHelperLocalMode(t)
|
||||
defer th.TearDown()
|
||||
clients := setupLocalClients(th)
|
||||
testData := setupData(t, th)
|
||||
extraSetup(t, th, testData)
|
||||
runTestCases(t, ttCases, testData, clients)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionsDuplicateBoardComment(t *testing.T) {
|
||||
extraSetup := func(t *testing.T, th *TestHelper, testData TestData) {
|
||||
err := th.Server.App().InsertBlock(model.Block{ID: "block-5", Title: "Test", Type: model.TypeComment, BoardID: testData.publicTemplate.ID}, userAdmin)
|
||||
require.NoError(t, err)
|
||||
err = th.Server.App().InsertBlock(model.Block{ID: "block-6", Title: "Test", Type: model.TypeComment, BoardID: testData.privateTemplate.ID}, userAdmin)
|
||||
require.NoError(t, err)
|
||||
err = th.Server.App().InsertBlock(model.Block{ID: "block-7", Title: "Test", Type: model.TypeComment, BoardID: testData.publicBoard.ID}, userAdmin)
|
||||
require.NoError(t, err)
|
||||
err = th.Server.App().InsertBlock(model.Block{ID: "block-8", Title: "Test", Type: model.TypeComment, BoardID: testData.privateBoard.ID}, userAdmin)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
ttCases := []TestCase{
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userCommenter, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userCommenter, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userCommenter, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userCommenter, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
|
||||
|
||||
// Invalid boardID/blockID combination
|
||||
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusNotFound, 0},
|
||||
|
@ -17,4 +17,6 @@ var (
|
||||
PermissionShareBoard = &mmModel.Permission{Id: "share_board", Name: "", Description: "", Scope: ""}
|
||||
PermissionManageBoardCards = &mmModel.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""}
|
||||
PermissionManageBoardProperties = &mmModel.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""}
|
||||
PermissionCommentBoardCards = &mmModel.Permission{Id: "comment_board_cards", Name: "", Description: "", Scope: ""}
|
||||
PermissionDeleteOthersComments = &mmModel.Permission{Id: "delete_others_comments", Name: "", Description: "", Scope: ""}
|
||||
)
|
||||
|
@ -67,10 +67,12 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
|
||||
}
|
||||
|
||||
switch permission {
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
|
||||
return member.SchemeAdmin
|
||||
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
|
||||
return member.SchemeAdmin || member.SchemeEditor
|
||||
case model.PermissionCommentBoardCards:
|
||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter
|
||||
case model.PermissionViewBoard:
|
||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
|
||||
default:
|
||||
|
@ -99,10 +99,12 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
|
||||
}
|
||||
|
||||
switch permission {
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
|
||||
return member.SchemeAdmin
|
||||
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
|
||||
return member.SchemeAdmin || member.SchemeEditor
|
||||
case model.PermissionCommentBoardCards:
|
||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter
|
||||
case model.PermissionViewBoard:
|
||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
|
||||
default:
|
||||
|
@ -61,6 +61,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
||||
}
|
||||
}, [card.title, title])
|
||||
const canEditBoardCards = useHasCurrentBoardPermissions([Permission.ManageBoardCards])
|
||||
const canCommentBoardCards = useHasCurrentBoardPermissions([Permission.CommentBoardCards])
|
||||
|
||||
const saveTitleRef = useRef<() => void>(saveTitle)
|
||||
saveTitleRef.current = saveTitle
|
||||
@ -207,7 +208,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
||||
comments={comments}
|
||||
boardId={card.boardId}
|
||||
cardId={card.id}
|
||||
readonly={props.readonly || !canEditBoardCards}
|
||||
readonly={props.readonly || !canCommentBoardCards}
|
||||
/>
|
||||
</Fragment>}
|
||||
</div>
|
||||
|
@ -57,6 +57,9 @@ describe('components/cardDetail/CommentsList', () => {
|
||||
board_id_1: {title: 'Board'},
|
||||
},
|
||||
current: 'board_id_1',
|
||||
myBoardMemberships: {
|
||||
['board_id_1']: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
@ -69,6 +72,9 @@ describe('components/cardDetail/CommentsList', () => {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
current: {id: 'team_id_1'},
|
||||
},
|
||||
})
|
||||
|
||||
const component = (
|
||||
@ -109,6 +115,18 @@ describe('components/cardDetail/CommentsList', () => {
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
board_id_1: {title: 'Board'},
|
||||
},
|
||||
current: 'board_id_1',
|
||||
myBoardMemberships: {
|
||||
['board_id_1']: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
current: {id: 'team_id_1'}
|
||||
}
|
||||
})
|
||||
|
||||
const component = (
|
||||
|
@ -13,6 +13,8 @@ import {MarkdownEditor} from '../markdownEditor'
|
||||
|
||||
import {IUser} from '../../user'
|
||||
import {getMe} from '../../store/users'
|
||||
import {useHasCurrentBoardPermissions} from '../../hooks/permissions'
|
||||
import {Permission} from '../../constants'
|
||||
|
||||
import AddCommentTourStep from '../onboardingTour/addComments/addComments'
|
||||
|
||||
@ -30,6 +32,7 @@ type Props = {
|
||||
const CommentsList = (props: Props) => {
|
||||
const [newComment, setNewComment] = useState('')
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const canDeleteOthersComments = useHasCurrentBoardPermissions([Permission.DeleteOthersComments])
|
||||
|
||||
const onSendClicked = () => {
|
||||
const commentText = newComment
|
||||
@ -88,15 +91,20 @@ const CommentsList = (props: Props) => {
|
||||
{/* New comment */}
|
||||
{!props.readonly && newCommentComponent}
|
||||
|
||||
{comments.slice(0).reverse().map((comment) => (
|
||||
<Comment
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
userImageUrl={Utils.getProfilePicture(comment.modifiedBy)}
|
||||
userId={comment.modifiedBy}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
))}
|
||||
{comments.slice(0).reverse().map((comment) => {
|
||||
|
||||
// Only modify _own_ comments, EXCEPT for Admins, which can delete _any_ comment
|
||||
// NOTE: editing comments will exist in the future (in addition to deleting)
|
||||
const canDeleteComment: boolean = canDeleteOthersComments || me?.id === comment.modifiedBy
|
||||
return (
|
||||
<Comment
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
userImageUrl={Utils.getProfilePicture(comment.modifiedBy)}
|
||||
userId={comment.modifiedBy}
|
||||
readonly={props.readonly || !canDeleteComment}
|
||||
/>
|
||||
)})}
|
||||
|
||||
{/* horizontal divider below comments */}
|
||||
{!(comments.length === 0 && props.readonly) && <hr className='CommentsList__divider'/>}
|
||||
|
@ -0,0 +1,683 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`src/components/shareBoard/userPermissionsRow should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="user-item"
|
||||
>
|
||||
<div
|
||||
class="user-item__content"
|
||||
>
|
||||
<div
|
||||
class="ml-3"
|
||||
>
|
||||
<strong />
|
||||
<strong
|
||||
class="ml-2 text-light"
|
||||
>
|
||||
@username_1
|
||||
</strong>
|
||||
<strong
|
||||
class="ml-2 text-light"
|
||||
>
|
||||
(You)
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="user-item__button"
|
||||
>
|
||||
Admin
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down CompassIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left "
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Viewer"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Viewer
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Commenter"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Commenter
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Editor"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Editor
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Admin"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<svg
|
||||
class="CheckIcon Icon"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline
|
||||
points="20,60 40,80 80,40"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="MenuOption MenuSeparator menu-separator"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Remove member"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Remove member
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/shareBoard/userPermissionsRow should match snapshot in plugin mode 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="user-item"
|
||||
>
|
||||
<div
|
||||
class="user-item__content"
|
||||
>
|
||||
<img
|
||||
class="user-item__img"
|
||||
/>
|
||||
<div
|
||||
class="ml-3"
|
||||
>
|
||||
<strong />
|
||||
<strong
|
||||
class="ml-2 text-light"
|
||||
>
|
||||
@username_1
|
||||
</strong>
|
||||
<strong
|
||||
class="ml-2 text-light"
|
||||
>
|
||||
(You)
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="user-item__button"
|
||||
>
|
||||
Admin
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down CompassIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left "
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Viewer"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Viewer
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Commenter"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Commenter
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Editor"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Editor
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Admin"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<svg
|
||||
class="CheckIcon Icon"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline
|
||||
points="20,60 40,80 80,40"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="MenuOption MenuSeparator menu-separator"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Remove member"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Remove member
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/shareBoard/userPermissionsRow should match snapshot in template 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="user-item"
|
||||
>
|
||||
<div
|
||||
class="user-item__content"
|
||||
>
|
||||
<img
|
||||
class="user-item__img"
|
||||
/>
|
||||
<div
|
||||
class="ml-3"
|
||||
>
|
||||
<strong />
|
||||
<strong
|
||||
class="ml-2 text-light"
|
||||
>
|
||||
@username_1
|
||||
</strong>
|
||||
<strong
|
||||
class="ml-2 text-light"
|
||||
>
|
||||
(You)
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="user-item__button"
|
||||
>
|
||||
Admin
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down CompassIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left "
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Viewer"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Viewer
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
aria-label="Editor"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Editor
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Admin"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<svg
|
||||
class="CheckIcon Icon"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline
|
||||
points="20,60 40,80 80,40"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="MenuOption MenuSeparator menu-separator"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Remove member"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Remove member
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
172
webapp/src/components/shareBoard/userPermissionsRow.test.tsx
Normal file
172
webapp/src/components/shareBoard/userPermissionsRow.test.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {act, render} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
import React from 'react'
|
||||
import {MemoryRouter} from 'react-router'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {BoardMember} from '../../blocks/board'
|
||||
|
||||
import {IUser} from '../../user'
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
import {mockStateStore, wrapDNDIntl} from '../../testUtils'
|
||||
import {Utils} from '../../utils'
|
||||
|
||||
import UserPermissionsRow from './userPermissionsRow'
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
const boardId = '1'
|
||||
|
||||
jest.mock('../../utils')
|
||||
|
||||
const mockedUtils = mocked(Utils, true)
|
||||
|
||||
const board = TestBlockFactory.createBoard()
|
||||
board.id = boardId
|
||||
board.teamId = 'team-id'
|
||||
board.channelId = 'channel_1'
|
||||
|
||||
describe('src/components/shareBoard/userPermissionsRow', () => {
|
||||
const me: IUser = {
|
||||
id: 'user-id-1',
|
||||
username: 'username_1',
|
||||
email: '',
|
||||
nickname: '',
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
props: {},
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
|
||||
const state = {
|
||||
teams: {
|
||||
current: {id: 'team-id', title: 'Test Team'},
|
||||
},
|
||||
users: {
|
||||
me,
|
||||
boardUsers: [me],
|
||||
blockSubscriptions: [],
|
||||
},
|
||||
boards: {
|
||||
current: board.id,
|
||||
boards: {
|
||||
[board.id]: board,
|
||||
},
|
||||
templates: [],
|
||||
membersInBoards: {
|
||||
[board.id]: {},
|
||||
},
|
||||
myBoardMemberships: {
|
||||
[board.id]: {userId: me.id, schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
let container: Element | undefined
|
||||
mockedUtils.isFocalboardPlugin.mockReturnValue(false)
|
||||
const store = mockStateStore([thunk], state)
|
||||
await act(async () => {
|
||||
const result = render(
|
||||
wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<UserPermissionsRow
|
||||
user={me}
|
||||
isMe={true}
|
||||
member={state.boards.myBoardMemberships[board.id] as BoardMember}
|
||||
teammateNameDisplay={'test'}
|
||||
onDeleteBoardMember={() => {}}
|
||||
onUpdateBoardMember={() => {}}
|
||||
/>
|
||||
</ReduxProvider>),
|
||||
{wrapper: MemoryRouter},
|
||||
)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = container?.querySelector('.user-item__button')
|
||||
expect(buttonElement).toBeDefined()
|
||||
userEvent.click(buttonElement!)
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot in plugin mode', async () => {
|
||||
let container: Element | undefined
|
||||
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
|
||||
const store = mockStateStore([thunk], state)
|
||||
await act(async () => {
|
||||
const result = render(
|
||||
wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<UserPermissionsRow
|
||||
user={me}
|
||||
isMe={true}
|
||||
member={state.boards.myBoardMemberships[board.id] as BoardMember}
|
||||
teammateNameDisplay={'test'}
|
||||
onDeleteBoardMember={() => {}}
|
||||
onUpdateBoardMember={() => {}}/>
|
||||
</ReduxProvider>),
|
||||
{wrapper: MemoryRouter},
|
||||
)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = container?.querySelector('.user-item__button')
|
||||
expect(buttonElement).toBeDefined()
|
||||
userEvent.click(buttonElement!)
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot in template', async () => {
|
||||
let container: Element | undefined
|
||||
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
|
||||
const testState = {
|
||||
...state,
|
||||
boards: {
|
||||
...state.boards,
|
||||
boards: {},
|
||||
templates: {
|
||||
[board.id]: {...board, isTemplate: true},
|
||||
}
|
||||
}
|
||||
}
|
||||
const store = mockStateStore([thunk], testState)
|
||||
await act(async () => {
|
||||
const result = render(
|
||||
wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<UserPermissionsRow
|
||||
user={me}
|
||||
isMe={true}
|
||||
member={state.boards.myBoardMemberships[board.id] as BoardMember}
|
||||
teammateNameDisplay={'test'}
|
||||
onDeleteBoardMember={() => {}}
|
||||
onUpdateBoardMember={() => {}}
|
||||
/>
|
||||
</ReduxProvider>),
|
||||
{wrapper: MemoryRouter},
|
||||
)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = container?.querySelector('.user-item__button')
|
||||
expect(buttonElement).toBeDefined()
|
||||
userEvent.click(buttonElement!)
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -14,6 +14,8 @@ import {BoardMember} from '../../blocks/board'
|
||||
import {IUser} from '../../user'
|
||||
import {Utils} from '../../utils'
|
||||
import {Permission} from '../../constants'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import {getCurrentBoard} from '../../store/boards'
|
||||
|
||||
import BoardPermissionGate from '../permissions/boardPermissionGate'
|
||||
|
||||
@ -28,6 +30,7 @@ type Props = {
|
||||
|
||||
const UserPermissionsRow = (props: Props): JSX.Element => {
|
||||
const intl = useIntl()
|
||||
const board = useAppSelector(getCurrentBoard)
|
||||
const {user, member, isMe, teammateNameDisplay} = props
|
||||
let currentRole = 'Viewer'
|
||||
if (member.schemeAdmin) {
|
||||
@ -71,6 +74,14 @@ const UserPermissionsRow = (props: Props): JSX.Element => {
|
||||
name={intl.formatMessage({id: 'BoardMember.schemeViewer', defaultMessage: 'Viewer'})}
|
||||
onClick={() => props.onUpdateBoardMember(member, 'Viewer')}
|
||||
/>
|
||||
{!board.isTemplate &&
|
||||
<Menu.Text
|
||||
id='Commenter'
|
||||
check={true}
|
||||
icon={currentRole === 'Commenter' ? <CheckIcon/> : null}
|
||||
name={intl.formatMessage({id: 'BoardMember.schemeCommenter', defaultMessage: 'Commenter'})}
|
||||
onClick={() => props.onUpdateBoardMember(member, 'Commenter')}
|
||||
/>}
|
||||
<Menu.Text
|
||||
id='Editor'
|
||||
check={true}
|
||||
|
@ -10,7 +10,9 @@ enum Permission {
|
||||
ManageBoardRoles = 'manage_board_roles',
|
||||
ManageBoardCards = 'manage_board_cards',
|
||||
ManageBoardProperties = 'manage_board_properties',
|
||||
CommentBoardCards = 'comment_board_cards',
|
||||
ViewBoard = 'view_board',
|
||||
DeleteOthersComments = 'delete_others_comments'
|
||||
}
|
||||
|
||||
class Constants {
|
||||
|
@ -22,8 +22,9 @@ export const useHasPermissions = (teamId: string, boardId: string, permissions:
|
||||
return true
|
||||
}
|
||||
|
||||
const adminPermissions = [Permission.ManageBoardType, Permission.DeleteBoard, Permission.ShareBoard, Permission.ManageBoardRoles]
|
||||
const adminPermissions = [Permission.ManageBoardType, Permission.DeleteBoard, Permission.ShareBoard, Permission.ManageBoardRoles, Permission.DeleteOthersComments]
|
||||
const editorPermissions = [Permission.ManageBoardCards, Permission.ManageBoardProperties]
|
||||
const commenterPermissions = [Permission.CommentBoardCards]
|
||||
const viewerPermissions = [Permission.ViewBoard]
|
||||
|
||||
for (const permission of permissions) {
|
||||
@ -33,6 +34,9 @@ export const useHasPermissions = (teamId: string, boardId: string, permissions:
|
||||
if (editorPermissions.includes(permission) && (member.schemeAdmin || member.schemeEditor)) {
|
||||
return true
|
||||
}
|
||||
if (commenterPermissions.includes(permission) && (member.schemeAdmin || member.schemeEditor || member.schemeCommenter)) {
|
||||
return true
|
||||
}
|
||||
if (viewerPermissions.includes(permission) && (member.schemeAdmin || member.schemeEditor || member.schemeCommenter || member.schemeViewer)) {
|
||||
return true
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user