You've already forked focalboard
							
							
				mirror of
				https://github.com/mattermost/focalboard.git
				synced 2025-10-31 00:17:42 +02:00 
			
		
		
		
	initial implementation of SysAdmin/TeamAdmin feature (#4537)
* initial implementation of SysAdmin/TeamAdmin feature * fix adminBadge tests * updating tests * more fixes for unit tests * lint fixes * update snapshots * update cypress test for call change * add additional unit tests * update test for lint errors * fix reviews implement tests * fix for merge, reset dialog before redirection * remove unused test code * fix more tests * fix swagger doc for missing parameters --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
		| @@ -206,6 +206,11 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: allow_admin | ||||
| 	//   in: path | ||||
| 	//   description: allows admin users to join private boards | ||||
| 	//   required: false | ||||
| 	//   type: boolean | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| @@ -222,6 +227,9 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	query := r.URL.Query() | ||||
| 	allowAdmin := query.Has("allow_admin") | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
| 	if userID == "" { | ||||
| 		a.errorResponse(w, r, model.NewErrBadRequest("missing user ID")) | ||||
| @@ -234,9 +242,14 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 		a.errorResponse(w, r, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	isAdmin := false | ||||
| 	if board.Type != model.BoardTypeOpen { | ||||
| 		a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board")) | ||||
| 		return | ||||
| 		if !allowAdmin || !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) { | ||||
| 			a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board")) | ||||
| 			return | ||||
| 		} | ||||
| 		isAdmin = true | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { | ||||
| @@ -257,7 +270,7 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	newBoardMember := &model.BoardMember{ | ||||
| 		UserID:          userID, | ||||
| 		BoardID:         boardID, | ||||
| 		SchemeAdmin:     board.MinimumRole == model.BoardRoleAdmin, | ||||
| 		SchemeAdmin:     board.MinimumRole == model.BoardRoleAdmin || isAdmin, | ||||
| 		SchemeEditor:    board.MinimumRole == model.BoardRoleNone || board.MinimumRole == model.BoardRoleEditor, | ||||
| 		SchemeCommenter: board.MinimumRole == model.BoardRoleCommenter, | ||||
| 		SchemeViewer:    board.MinimumRole == model.BoardRoleViewer, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| @@ -15,6 +16,7 @@ func (a *API) registerTeamsRoutes(r *mux.Router) { | ||||
| 	r.HandleFunc("/teams", a.sessionRequired(a.handleGetTeams)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}", a.sessionRequired(a.handleGetTeam)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsers)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsersByID)).Methods("POST") | ||||
| 	r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET") | ||||
| } | ||||
|  | ||||
| @@ -257,3 +259,106 @@ func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) { | ||||
| 	auditRec.AddMeta("userCount", len(users)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetTeamUsersByID(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /teams/{teamID}/users getTeamUsersByID | ||||
| 	// | ||||
| 	// Returns a user[] | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: []UserIDs to return | ||||
| 	//   required: true | ||||
| 	//   type: []string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/User" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	requestBody, err := io.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var userIDs []string | ||||
| 	if err = json.Unmarshal(requestBody, &userIDs); err != nil { | ||||
| 		a.errorResponse(w, r, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getTeamUsersByID", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r, model.NewErrPermission("access denied to team")) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var users []*model.User | ||||
| 	var error error | ||||
|  | ||||
| 	if len(userIDs) == 0 { | ||||
| 		a.errorResponse(w, r, model.NewErrBadRequest("User IDs are empty")) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if userIDs[0] == model.SingleUser { | ||||
| 		ws, _ := a.app.GetRootTeam() | ||||
| 		now := utils.GetMillis() | ||||
| 		user := &model.User{ | ||||
| 			ID:       model.SingleUser, | ||||
| 			Username: model.SingleUser, | ||||
| 			Email:    model.SingleUser, | ||||
| 			CreateAt: ws.UpdateAt, | ||||
| 			UpdateAt: now, | ||||
| 		} | ||||
| 		users = append(users, user) | ||||
| 	} else { | ||||
| 		users, error = a.app.GetUsersList(userIDs) | ||||
| 		if error != nil { | ||||
| 			a.errorResponse(w, r, error) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		for i, u := range users { | ||||
| 			if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) { | ||||
| 				users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id) | ||||
| 			} | ||||
| 			if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) { | ||||
| 				users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	usersList, err := json.Marshal(users) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonStringResponse(w, http.StatusOK, string(usersList)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|   | ||||
| @@ -107,6 +107,12 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: false | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| @@ -118,6 +124,8 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
| 	query := r.URL.Query() | ||||
| 	teamID := query.Get("teamID") | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| @@ -146,6 +154,13 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if teamID != "" && a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionManageTeam) { | ||||
| 		user.Permissions = append(user.Permissions, model.PermissionManageTeam.Id) | ||||
| 	} | ||||
| 	if a.permissions.HasPermissionTo(userID, model.PermissionManageSystem) { | ||||
| 		user.Permissions = append(user.Permissions, model.PermissionManageSystem.Id) | ||||
| 	} | ||||
|  | ||||
| 	userData, err := json.Marshal(user) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r, err) | ||||
|   | ||||
| @@ -502,11 +502,48 @@ func (a *App) DeleteBoard(boardID, userID string) error { | ||||
| } | ||||
|  | ||||
| func (a *App) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) { | ||||
| 	return a.store.GetMembersForBoard(boardID) | ||||
| 	members, err := a.store.GetMembersForBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	board, err := a.store.GetBoard(boardID) | ||||
| 	if err != nil && !model.IsErrNotFound(err) { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if board != nil { | ||||
| 		for i, m := range members { | ||||
| 			if !m.SchemeAdmin { | ||||
| 				if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) { | ||||
| 					members[i].SchemeAdmin = true | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return members, nil | ||||
| } | ||||
|  | ||||
| func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) { | ||||
| 	return a.store.GetMembersForUser(userID) | ||||
| 	members, err := a.store.GetMembersForUser(userID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	for i, m := range members { | ||||
| 		if !m.SchemeAdmin { | ||||
| 			board, err := a.store.GetBoard(m.BoardID) | ||||
| 			if err != nil && !model.IsErrNotFound(err) { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			if board != nil { | ||||
| 				if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) { | ||||
| 					// if system/team admin | ||||
| 					members[i].SchemeAdmin = true | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return members, nil | ||||
| } | ||||
|  | ||||
| func (a *App) GetMemberForBoard(boardID string, userID string) (*model.BoardMember, error) { | ||||
| @@ -536,6 +573,14 @@ func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, e | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if !newMember.SchemeAdmin { | ||||
| 		if board != nil { | ||||
| 			if a.permissions.HasPermissionToTeam(newMember.UserID, board.TeamID, model.PermissionManageTeam) { | ||||
| 				newMember.SchemeAdmin = true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if !board.IsTemplate { | ||||
| 		if err = a.addBoardsToDefaultCategory(member.UserID, board.TeamID, []*model.Board{board}); err != nil { | ||||
| 			return nil, err | ||||
|   | ||||
| @@ -127,6 +127,7 @@ func TestAddMemberToBoard(t *testing.T) { | ||||
| 			}, | ||||
| 		}, nil).Times(2) | ||||
| 		th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "default_category_id", []string{"board_id_1"}).Return(nil) | ||||
| 		th.API.EXPECT().HasPermissionToTeam("user_id_1", "team_id_1", model.PermissionManageTeam).Return(false).Times(1) | ||||
|  | ||||
| 		addedBoardMember, err := th.App.AddMemberToBoard(boardMember) | ||||
| 		require.NoError(t, err) | ||||
| @@ -180,7 +181,7 @@ func TestPatchBoard(t *testing.T) { | ||||
| 			ID:         boardID, | ||||
| 			TeamID:     teamID, | ||||
| 			IsTemplate: true, | ||||
| 		}, nil) | ||||
| 		}, nil).Times(2) | ||||
|  | ||||
| 		// Type not null will retrieve team members | ||||
| 		th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil) | ||||
| @@ -218,7 +219,7 @@ func TestPatchBoard(t *testing.T) { | ||||
| 			ID:         boardID, | ||||
| 			TeamID:     teamID, | ||||
| 			IsTemplate: true, | ||||
| 		}, nil) | ||||
| 		}, nil).Times(2) | ||||
|  | ||||
| 		// Type not null will retrieve team members | ||||
| 		th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil) | ||||
| @@ -256,7 +257,7 @@ func TestPatchBoard(t *testing.T) { | ||||
| 			ID:         boardID, | ||||
| 			TeamID:     teamID, | ||||
| 			IsTemplate: true, | ||||
| 		}, nil) | ||||
| 		}, nil).Times(2) | ||||
| 		// Type not null will retrieve team members | ||||
| 		th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) | ||||
|  | ||||
| @@ -294,7 +295,7 @@ func TestPatchBoard(t *testing.T) { | ||||
| 			ID:         boardID, | ||||
| 			TeamID:     teamID, | ||||
| 			IsTemplate: true, | ||||
| 		}, nil) | ||||
| 		}, nil).Times(2) | ||||
| 		// Type not null will retrieve team members | ||||
| 		th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) | ||||
|  | ||||
| @@ -332,7 +333,10 @@ func TestPatchBoard(t *testing.T) { | ||||
| 			ID:         boardID, | ||||
| 			TeamID:     teamID, | ||||
| 			IsTemplate: true, | ||||
| 		}, nil) | ||||
| 		}, nil).Times(3) | ||||
|  | ||||
| 		th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) | ||||
|  | ||||
| 		// Type not null will retrieve team members | ||||
| 		th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) | ||||
|  | ||||
| @@ -370,7 +374,11 @@ func TestPatchBoard(t *testing.T) { | ||||
| 			ID:         boardID, | ||||
| 			TeamID:     teamID, | ||||
| 			IsTemplate: true, | ||||
| 		}, nil) | ||||
| 			ChannelID:  "", | ||||
| 		}, nil).Times(1) | ||||
|  | ||||
| 		th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) | ||||
|  | ||||
| 		// Type not null will retrieve team members | ||||
| 		th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) | ||||
|  | ||||
| @@ -566,3 +574,99 @@ func TestDuplicateBoard(t *testing.T) { | ||||
| 		assert.NotNil(t, members) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestGetMembersForBoard(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	const boardID = "board_id_1" | ||||
| 	const userID = "user_id_1" | ||||
| 	const teamID = "team_id_1" | ||||
|  | ||||
| 	th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{ | ||||
| 		{ | ||||
| 			BoardID:      boardID, | ||||
| 			UserID:       userID, | ||||
| 			SchemeEditor: true, | ||||
| 		}, | ||||
| 	}, nil).Times(3) | ||||
| 	th.Store.EXPECT().GetBoard(boardID).Return(nil, nil).Times(1) | ||||
| 	t.Run("-base case", func(t *testing.T) { | ||||
| 		members, err := th.App.GetMembersForBoard(boardID) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotNil(t, members) | ||||
| 		assert.False(t, members[0].SchemeAdmin) | ||||
| 	}) | ||||
|  | ||||
| 	board := &model.Board{ | ||||
| 		ID:     boardID, | ||||
| 		TeamID: teamID, | ||||
| 	} | ||||
| 	th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2) | ||||
| 	th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) | ||||
|  | ||||
| 	t.Run("-team check false ", func(t *testing.T) { | ||||
| 		members, err := th.App.GetMembersForBoard(boardID) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotNil(t, members) | ||||
|  | ||||
| 		assert.False(t, members[0].SchemeAdmin) | ||||
| 	}) | ||||
|  | ||||
| 	th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) | ||||
| 	t.Run("-team check true", func(t *testing.T) { | ||||
| 		members, err := th.App.GetMembersForBoard(boardID) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotNil(t, members) | ||||
|  | ||||
| 		assert.True(t, members[0].SchemeAdmin) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestGetMembersForUser(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	const boardID = "board_id_1" | ||||
| 	const userID = "user_id_1" | ||||
| 	const teamID = "team_id_1" | ||||
|  | ||||
| 	th.Store.EXPECT().GetMembersForUser(userID).Return([]*model.BoardMember{ | ||||
| 		{ | ||||
| 			BoardID:      boardID, | ||||
| 			UserID:       userID, | ||||
| 			SchemeEditor: true, | ||||
| 		}, | ||||
| 	}, nil).Times(3) | ||||
| 	th.Store.EXPECT().GetBoard(boardID).Return(nil, nil) | ||||
| 	t.Run("-base case", func(t *testing.T) { | ||||
| 		members, err := th.App.GetMembersForUser(userID) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotNil(t, members) | ||||
| 		assert.False(t, members[0].SchemeAdmin) | ||||
| 	}) | ||||
|  | ||||
| 	board := &model.Board{ | ||||
| 		ID:     boardID, | ||||
| 		TeamID: teamID, | ||||
| 	} | ||||
| 	th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2) | ||||
|  | ||||
| 	th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) | ||||
| 	t.Run("-team check false ", func(t *testing.T) { | ||||
| 		members, err := th.App.GetMembersForUser(userID) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotNil(t, members) | ||||
|  | ||||
| 		assert.False(t, members[0].SchemeAdmin) | ||||
| 	}) | ||||
|  | ||||
| 	th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) | ||||
| 	t.Run("-team check true", func(t *testing.T) { | ||||
| 		members, err := th.App.GetMembersForUser(userID) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotNil(t, members) | ||||
|  | ||||
| 		assert.True(t, members[0].SchemeAdmin) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -58,6 +58,7 @@ func TestGetUserCategoryBoards(t *testing.T) { | ||||
| 				Synthetic: false, | ||||
| 			}, | ||||
| 		}, nil) | ||||
| 		th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) | ||||
| 		th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil) | ||||
|  | ||||
| 		categoryBoards, err := th.App.GetUserCategoryBoards("user_id", "team_id") | ||||
| @@ -151,6 +152,7 @@ func TestCreateBoardsCategory(t *testing.T) { | ||||
| 				Synthetic: true, | ||||
| 			}, | ||||
| 		}, nil) | ||||
| 		th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) | ||||
|  | ||||
| 		existingCategoryBoards := []model.CategoryBoards{} | ||||
| 		boardsCategory, err := th.App.createBoardsCategory("user_id", "team_id", existingCategoryBoards) | ||||
| @@ -195,6 +197,7 @@ func TestCreateBoardsCategory(t *testing.T) { | ||||
| 				Synthetic: false, | ||||
| 			}, | ||||
| 		}, nil) | ||||
| 		th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) | ||||
| 		th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil) | ||||
|  | ||||
| 		th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ | ||||
| @@ -244,6 +247,7 @@ func TestCreateBoardsCategory(t *testing.T) { | ||||
| 				Synthetic: true, | ||||
| 			}, | ||||
| 		}, nil) | ||||
| 		th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) | ||||
| 		th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1"}).Return(nil) | ||||
|  | ||||
| 		th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ | ||||
|   | ||||
| @@ -10,6 +10,9 @@ import ( | ||||
| 	"github.com/mattermost/focalboard/server/auth" | ||||
| 	"github.com/mattermost/focalboard/server/services/config" | ||||
| 	"github.com/mattermost/focalboard/server/services/metrics" | ||||
| 	"github.com/mattermost/focalboard/server/services/permissions/mmpermissions" | ||||
| 	mmpermissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mmpermissions/mocks" | ||||
| 	permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks" | ||||
| 	"github.com/mattermost/focalboard/server/services/store/mockstore" | ||||
| 	"github.com/mattermost/focalboard/server/services/webhook" | ||||
| 	"github.com/mattermost/focalboard/server/ws" | ||||
| @@ -23,6 +26,7 @@ type TestHelper struct { | ||||
| 	Store        *mockstore.MockStore | ||||
| 	FilesBackend *mocks.FileBackend | ||||
| 	logger       mlog.LoggerIFace | ||||
| 	API          *mmpermissionsMocks.MockAPI | ||||
| } | ||||
|  | ||||
| func SetupTestHelper(t *testing.T) (*TestHelper, func()) { | ||||
| @@ -37,6 +41,10 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) { | ||||
| 	webhook := webhook.NewClient(&cfg, logger) | ||||
| 	metricsService := metrics.NewMetrics(metrics.InstanceInfo{}) | ||||
|  | ||||
| 	mockStore := permissionsMocks.NewMockStore(ctrl) | ||||
| 	mockAPI := mmpermissionsMocks.NewMockAPI(ctrl) | ||||
| 	permissions := mmpermissions.New(mockStore, mockAPI, mlog.CreateConsoleTestLogger(true, mlog.LvlError)) | ||||
|  | ||||
| 	appServices := Services{ | ||||
| 		Auth:             auth, | ||||
| 		Store:            store, | ||||
| @@ -45,6 +53,7 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) { | ||||
| 		Metrics:          metricsService, | ||||
| 		Logger:           logger, | ||||
| 		SkipTemplateInit: true, | ||||
| 		Permissions:      permissions, | ||||
| 	} | ||||
| 	app2 := New(&cfg, wsserver, appServices) | ||||
|  | ||||
| @@ -60,5 +69,6 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) { | ||||
| 		Store:        store, | ||||
| 		FilesBackend: filesBackend, | ||||
| 		logger:       logger, | ||||
| 		API:          mockAPI, | ||||
| 	}, tearDown | ||||
| } | ||||
|   | ||||
| @@ -39,7 +39,7 @@ func TestPrepareOnboardingTour(t *testing.T) { | ||||
| 			nil, nil) | ||||
| 		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(welcomeBoard.ID).Return(&welcomeBoard, nil).Times(2) | ||||
| 		th.Store.EXPECT().GetBoard("board_id_2").Return(&welcomeBoard, nil).Times(1) | ||||
| 		th.Store.EXPECT().GetUsersByTeam("0", "", false, false).Return([]*model.User{}, nil) | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,20 @@ func (a *App) GetTeamUsers(teamID string, asGuestID string) ([]*model.User, erro | ||||
| } | ||||
|  | ||||
| func (a *App) SearchTeamUsers(teamID string, searchQuery string, asGuestID string, excludeBots bool) ([]*model.User, error) { | ||||
| 	return a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID, excludeBots, a.config.ShowEmailAddress, a.config.ShowFullName) | ||||
| 	users, err := a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID, excludeBots, a.config.ShowEmailAddress, a.config.ShowFullName) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	for i, u := range users { | ||||
| 		if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) { | ||||
| 			users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id) | ||||
| 		} | ||||
| 		if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) { | ||||
| 			users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id) | ||||
| 		} | ||||
| 	} | ||||
| 	return users, nil | ||||
| } | ||||
|  | ||||
| func (a *App) UpdateUserConfig(userID string, patch model.UserPreferencesPatch) ([]mmModel.Preference, error) { | ||||
|   | ||||
							
								
								
									
										64
									
								
								server/app/user_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								server/app/user_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestSearchUsers(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
| 	th.App.config.ShowEmailAddress = false | ||||
| 	th.App.config.ShowFullName = false | ||||
|  | ||||
| 	teamID := "team-id-1" | ||||
| 	userID := "user-id-1" | ||||
|  | ||||
| 	t.Run("return empty users", func(t *testing.T) { | ||||
| 		th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{}, nil) | ||||
|  | ||||
| 		users, err := th.App.SearchTeamUsers(teamID, "", "", true) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, 0, len(users)) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("return user", func(t *testing.T) { | ||||
| 		th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil) | ||||
| 		th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) | ||||
| 		th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(false).Times(1) | ||||
|  | ||||
| 		users, err := th.App.SearchTeamUsers(teamID, "", "", true) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, 1, len(users)) | ||||
| 		assert.Equal(t, 0, len(users[0].Permissions)) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("return team admin", func(t *testing.T) { | ||||
| 		th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil) | ||||
| 		th.App.config.ShowEmailAddress = false | ||||
| 		th.App.config.ShowFullName = false | ||||
| 		th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) | ||||
| 		th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(false).Times(1) | ||||
|  | ||||
| 		users, err := th.App.SearchTeamUsers(teamID, "", "", true) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, 1, len(users)) | ||||
| 		assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("return system admin", func(t *testing.T) { | ||||
| 		th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil) | ||||
| 		th.App.config.ShowEmailAddress = false | ||||
| 		th.App.config.ShowFullName = false | ||||
| 		th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) | ||||
| 		th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(true).Times(1) | ||||
|  | ||||
| 		users, err := th.App.SearchTeamUsers(teamID, "", "", true) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, 1, len(users)) | ||||
| 		assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id) | ||||
| 		assert.Equal(t, users[0].Permissions[1], model.PermissionManageSystem.Id) | ||||
| 	}) | ||||
| } | ||||
| @@ -78,6 +78,9 @@ func (*FakePermissionPluginAPI) HasPermissionTo(userID string, permission *mmMod | ||||
| } | ||||
|  | ||||
| func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool { | ||||
| 	if permission.Id == model.PermissionManageTeam.Id { | ||||
| 		return false | ||||
| 	} | ||||
| 	if userID == userNoTeamMember { | ||||
| 		return false | ||||
| 	} | ||||
|   | ||||
| @@ -6,6 +6,8 @@ import ( | ||||
|  | ||||
| var ( | ||||
| 	PermissionViewTeam              = mmModel.PermissionViewTeam | ||||
| 	PermissionManageTeam            = mmModel.PermissionManageTeam | ||||
| 	PermissionManageSystem          = mmModel.PermissionManageSystem | ||||
| 	PermissionReadChannel           = mmModel.PermissionReadChannel | ||||
| 	PermissionViewMembers           = mmModel.PermissionViewMembers | ||||
| 	PermissionCreatePublicChannel   = mmModel.PermissionCreatePublicChannel | ||||
|   | ||||
| @@ -66,6 +66,9 @@ type User struct { | ||||
| 	// required: true | ||||
| 	IsGuest bool `json:"is_guest"` | ||||
|  | ||||
| 	// Special Permissions the user may have | ||||
| 	Permissions []string `json:"permissions,omitempty"` | ||||
|  | ||||
| 	Roles string `json:"roles"` | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -31,6 +31,9 @@ func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel | ||||
| 	if userID == "" || teamID == "" || permission == nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	if permission.Id == model.PermissionManageTeam.Id { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -27,6 +27,11 @@ func TestHasPermissionToTeam(t *testing.T) { | ||||
| 		hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageBoardCards) | ||||
| 		assert.True(t, hasPermission) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("no users have PermissionManageTeam on teams", func(t *testing.T) { | ||||
| 		hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageTeam) | ||||
| 		assert.False(t, hasPermission) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestHasPermissionToBoard(t *testing.T) { | ||||
| @@ -141,4 +146,27 @@ func TestHasPermissionToBoard(t *testing.T) { | ||||
|  | ||||
| 		th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Manage Team Permission ", func(t *testing.T) { | ||||
| 		member := &model.BoardMember{ | ||||
| 			UserID:       "user-id", | ||||
| 			BoardID:      "board-id", | ||||
| 			SchemeViewer: true, | ||||
| 		} | ||||
|  | ||||
| 		hasPermissionTo := []*mmModel.Permission{ | ||||
| 			model.PermissionViewBoard, | ||||
| 		} | ||||
|  | ||||
| 		hasNotPermissionTo := []*mmModel.Permission{ | ||||
| 			model.PermissionManageBoardType, | ||||
| 			model.PermissionDeleteBoard, | ||||
| 			model.PermissionManageBoardRoles, | ||||
| 			model.PermissionShareBoard, | ||||
| 			model.PermissionManageBoardCards, | ||||
| 			model.PermissionManageBoardProperties, | ||||
| 		} | ||||
|  | ||||
| 		th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -58,6 +58,13 @@ func (th *TestHelper) checkBoardPermissions(roleName string, member *model.Board | ||||
| 				Return(member, nil). | ||||
| 				Times(1) | ||||
|  | ||||
| 			if !member.SchemeAdmin { | ||||
| 				th.api.EXPECT(). | ||||
| 					HasPermissionToTeam(member.UserID, teamID, model.PermissionManageTeam). | ||||
| 					Return(roleName == "elevated-admin"). | ||||
| 					Times(1) | ||||
| 			} | ||||
|  | ||||
| 			hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) | ||||
| 			assert.True(t, hasPermission) | ||||
| 		}) | ||||
| @@ -80,6 +87,13 @@ func (th *TestHelper) checkBoardPermissions(roleName string, member *model.Board | ||||
| 				Return(member, nil). | ||||
| 				Times(1) | ||||
|  | ||||
| 			if !member.SchemeAdmin { | ||||
| 				th.api.EXPECT(). | ||||
| 					HasPermissionToTeam(member.UserID, teamID, model.PermissionManageTeam). | ||||
| 					Return(roleName == "elevated-admin"). | ||||
| 					Times(1) | ||||
| 			} | ||||
|  | ||||
| 			hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) | ||||
| 			assert.False(t, hasPermission) | ||||
| 		}) | ||||
|   | ||||
| @@ -82,7 +82,6 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod | ||||
| 	if !s.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	member, err := s.store.GetMemberForBoard(boardID, userID) | ||||
| 	if model.IsErrNotFound(err) { | ||||
| 		return false | ||||
| @@ -107,6 +106,13 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod | ||||
| 		member.SchemeViewer = true | ||||
| 	} | ||||
|  | ||||
| 	// Admins become member of boards, but get minimal role | ||||
| 	// if they are a System/Team Admin (model.PermissionManageTeam) | ||||
| 	// elevate their permissions | ||||
| 	if !member.SchemeAdmin && s.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	switch permission { | ||||
| 	case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments: | ||||
| 		return member.SchemeAdmin | ||||
|   | ||||
| @@ -219,4 +219,25 @@ func TestHasPermissionToBoard(t *testing.T) { | ||||
|  | ||||
| 		th.checkBoardPermissions("viewer", member, teamID, hasPermissionTo, hasNotPermissionTo) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("elevate board viewer permissions", func(t *testing.T) { | ||||
| 		member := &model.BoardMember{ | ||||
| 			UserID:       userID, | ||||
| 			BoardID:      boardID, | ||||
| 			SchemeViewer: true, | ||||
| 		} | ||||
|  | ||||
| 		hasPermissionTo := []*mmModel.Permission{ | ||||
| 			model.PermissionManageBoardType, | ||||
| 			model.PermissionDeleteBoard, | ||||
| 			model.PermissionManageBoardRoles, | ||||
| 			model.PermissionShareBoard, | ||||
| 			model.PermissionManageBoardCards, | ||||
| 			model.PermissionViewBoard, | ||||
| 			model.PermissionManageBoardProperties, | ||||
| 		} | ||||
|  | ||||
| 		hasNotPermissionTo := []*mmModel.Permission{} | ||||
| 		th.checkBoardPermissions("elevated-admin", member, teamID, hasPermissionTo, hasNotPermissionTo) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -1991,6 +1991,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select | ||||
|                             > | ||||
|                               @username_1 | ||||
|                             </strong> | ||||
|                             <div | ||||
|                               class="AdminBadge" | ||||
|                             > | ||||
|                               <div | ||||
|                                 class="AdminBadge__box" | ||||
|                               > | ||||
|                                 Team Admin | ||||
|                               </div> | ||||
|                             </div> | ||||
|                           </div> | ||||
|                         </div> | ||||
|                       </div> | ||||
| @@ -2017,6 +2026,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select | ||||
|                             > | ||||
|                               @username_2 | ||||
|                             </strong> | ||||
|                             <div | ||||
|                               class="AdminBadge" | ||||
|                             > | ||||
|                               <div | ||||
|                                 class="AdminBadge__box" | ||||
|                               > | ||||
|                                 Admin | ||||
|                               </div> | ||||
|                             </div> | ||||
|                           </div> | ||||
|                         </div> | ||||
|                       </div> | ||||
|   | ||||
| @@ -728,3 +728,263 @@ exports[`src/components/shareBoard/userPermissionsRow should match snapshot in t | ||||
|   </div> | ||||
| </div> | ||||
| `; | ||||
|  | ||||
| exports[`src/components/shareBoard/userPermissionsRow should match snapshot-admin 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 | ||||
|           class="AdminBadge" | ||||
|         > | ||||
|           <div | ||||
|             class="AdminBadge__box" | ||||
|           > | ||||
|             Admin | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div> | ||||
|       <div | ||||
|         aria-label="menuwrapper" | ||||
|         class="MenuWrapper override menuOpened" | ||||
|         role="button" | ||||
|       > | ||||
|         <button | ||||
|           class="user-item__button" | ||||
|         > | ||||
|           Admin | ||||
|           <i | ||||
|             class="CompassIcon icon-chevron-down CompassIcon" | ||||
|           /> | ||||
|         </button> | ||||
|         <div | ||||
|           class="Menu noselect left " | ||||
|           style="top: 40px;" | ||||
|         > | ||||
|           <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="menu-option__icon" | ||||
|                     > | ||||
|                       <div | ||||
|                         class="empty-icon" | ||||
|                       /> | ||||
|                     </div> | ||||
|                   </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="menu-option__icon" | ||||
|                     > | ||||
|                       <div | ||||
|                         class="empty-icon" | ||||
|                       /> | ||||
|                     </div> | ||||
|                   </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="menu-option__icon" | ||||
|                     > | ||||
|                       <div | ||||
|                         class="empty-icon" | ||||
|                       /> | ||||
|                     </div> | ||||
|                   </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" | ||||
|                   > | ||||
|                     <div | ||||
|                       class="menu-option__icon" | ||||
|                     > | ||||
|                       <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> | ||||
|                   <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> | ||||
| `; | ||||
|   | ||||
| @@ -548,8 +548,8 @@ describe('src/components/shareBoard/shareBoard', () => { | ||||
|         } | ||||
|         mockedOctoClient.getSharing.mockResolvedValue(sharing) | ||||
|         const users: IUser[] = [ | ||||
|             {id: 'userid1', username: 'username_1'} as IUser, | ||||
|             {id: 'userid2', username: 'username_2'} as IUser, | ||||
|             {id: 'userid1', username: 'username_1', permissions: ['manage_team']} as IUser, | ||||
|             {id: 'userid2', username: 'username_2', permissions: ['manage_system']} as IUser, | ||||
|             {id: 'userid3', username: 'username_3'} as IUser, | ||||
|             {id: 'userid4', username: 'username_4'} as IUser, | ||||
|         ] | ||||
|   | ||||
| @@ -32,6 +32,7 @@ import Button from '../../widgets/buttons/button' | ||||
| import {sendFlashMessage} from '../flashMessages' | ||||
| import {Permission} from '../../constants' | ||||
| import GuestBadge from '../../widgets/guestBadge' | ||||
| import AdminBadge from '../../widgets/adminBadge/adminBadge' | ||||
|  | ||||
| import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient' | ||||
|  | ||||
| @@ -310,6 +311,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element { | ||||
|                         <strong>{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}</strong> | ||||
|                         <strong className='ml-2 text-light'>{`@${user.username}`}</strong> | ||||
|                         <GuestBadge show={Boolean(user?.is_guest)}/> | ||||
|                         <AdminBadge permissions={user.permissions}/> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             ) | ||||
|   | ||||
| @@ -104,6 +104,38 @@ describe('src/components/shareBoard/userPermissionsRow', () => { | ||||
|         expect(container).toMatchSnapshot() | ||||
|     }) | ||||
|  | ||||
|     test('should match snapshot-admin', async () => { | ||||
|         let container: Element | undefined | ||||
|         mockedUtils.isFocalboardPlugin.mockReturnValue(false) | ||||
|         const store = mockStateStore([thunk], state) | ||||
|  | ||||
|         const newMe = Object.assign({}, me) | ||||
|         newMe.permissions = ['manage_system'] | ||||
|         await act(async () => { | ||||
|             const result = render( | ||||
|                 wrapDNDIntl( | ||||
|                     <ReduxProvider store={store}> | ||||
|                         <UserPermissionsRow | ||||
|                             user={newMe} | ||||
|                             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) | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import {IUser} from '../../user' | ||||
| import {Utils} from '../../utils' | ||||
| import {Permission} from '../../constants' | ||||
| import GuestBadge from '../../widgets/guestBadge' | ||||
| import AdminBadge from '../../widgets/adminBadge/adminBadge' | ||||
| import {useAppSelector} from '../../store/hooks' | ||||
| import {getCurrentBoard} from '../../store/boards' | ||||
|  | ||||
| @@ -65,6 +66,7 @@ const UserPermissionsRow = (props: Props): JSX.Element => { | ||||
|                     <strong className='ml-2 text-light'>{`@${user.username}`}</strong> | ||||
|                     {isMe && <strong className='ml-2 text-light'>{intl.formatMessage({id: 'ShareBoard.userPermissionsYouText', defaultMessage: '(You)'})}</strong>} | ||||
|                     <GuestBadge show={user.is_guest}/> | ||||
|                     <AdminBadge permissions={user.permissions}/> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div> | ||||
|   | ||||
| @@ -160,7 +160,10 @@ class OctoClient { | ||||
|     } | ||||
|  | ||||
|     async getMe(): Promise<IUser | undefined> { | ||||
|         const path = '/api/v2/users/me' | ||||
|         let path = '/api/v2/users/me' | ||||
|         if (this.teamId !== Constants.globalTeamId) { | ||||
|             path += `?teamID=${this.teamId}` | ||||
|         } | ||||
|         const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) | ||||
|         if (response.status !== 200) { | ||||
|             return undefined | ||||
| @@ -467,12 +470,15 @@ class OctoClient { | ||||
|         return this.getJson<BoardMember>(response, {} as BoardMember) | ||||
|     } | ||||
|  | ||||
|     async joinBoard(boardId: string): Promise<BoardMember|undefined> { | ||||
|     async joinBoard(boardId: string, allowAdmin: boolean): Promise<BoardMember|undefined> { | ||||
|         Utils.log(`joinBoard: board ${boardId}`) | ||||
|  | ||||
|         const response = await fetch(this.getBaseURL() + `/api/v2/boards/${boardId}/join`, { | ||||
|             method: 'POST', | ||||
|         let path = `/api/v2/boards/${boardId}/join` | ||||
|         if (allowAdmin) { | ||||
|             path += '?allow_admin' | ||||
|         } | ||||
|         const response = await fetch(this.getBaseURL() + path, { | ||||
|             headers: this.headers(), | ||||
|             method: 'POST', | ||||
|         }) | ||||
|  | ||||
|         if (response.status !== 200) { | ||||
| @@ -680,6 +686,22 @@ class OctoClient { | ||||
|         return (await this.getJson(response, [])) as IUser[] | ||||
|     } | ||||
|  | ||||
|     async getTeamUsersList(userIds: string[], teamId: string): Promise<IUser[] | []> { | ||||
|         const path = this.teamPath(teamId) + '/users' | ||||
|         const body = JSON.stringify(userIds) | ||||
|         const response = await fetch(this.getBaseURL() + path, { | ||||
|             headers: this.headers(), | ||||
|             method: 'POST', | ||||
|             body, | ||||
|         }) | ||||
|  | ||||
|         if (response.status !== 200) { | ||||
|             return [] | ||||
|         } | ||||
|  | ||||
|         return (await this.getJson(response, [])) as IUser[] | ||||
|     } | ||||
|  | ||||
|     async searchTeamUsers(searchQuery: string, excludeBots?: boolean): Promise<IUser[]> { | ||||
|         let path = this.teamPath() + `/users?search=${searchQuery}` | ||||
|         if (excludeBots) { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| import React, {useEffect, useState, useMemo, useCallback} from 'react' | ||||
| import {batch} from 'react-redux' | ||||
| import {FormattedMessage, useIntl} from 'react-intl' | ||||
| import {useRouteMatch} from 'react-router-dom' | ||||
| import {useRouteMatch, useHistory} from 'react-router-dom' | ||||
|  | ||||
| import Workspace from '../../components/workspace' | ||||
| import CloudMessage from '../../components/messages/cloudMessage' | ||||
| @@ -29,6 +29,7 @@ import { | ||||
|     addMyBoardMemberships, | ||||
| } from '../../store/boards' | ||||
| import {getCurrentViewId, setCurrent as setCurrentView, updateViews} from '../../store/views' | ||||
| import ConfirmationDialog from '../../components/confirmationDialogBox' | ||||
| import {initialLoad, initialReadOnlyLoad, loadBoardData} from '../../store/initialLoad' | ||||
| import {useAppSelector, useAppDispatch} from '../../store/hooks' | ||||
| import {setTeam} from '../../store/teams' | ||||
| @@ -79,6 +80,8 @@ const BoardPage = (props: Props): JSX.Element => { | ||||
|     const me = useAppSelector<IUser|null>(getMe) | ||||
|     const hiddenBoardIDs = useAppSelector(getHiddenBoardIDs) | ||||
|     const category = useAppSelector(getCategoryOfBoard(activeBoardId)) | ||||
|     const [showJoinBoardDialog, setShowJoinBoardDialog] = useState<boolean>(false) | ||||
|     const history = useHistory() | ||||
|  | ||||
|     // if we're in a legacy route and not showing a shared board, | ||||
|     // redirect to the new URL schema equivalent | ||||
| @@ -177,18 +180,40 @@ const BoardPage = (props: Props): JSX.Element => { | ||||
|         } | ||||
|     }, [me?.id, activeBoardId]) | ||||
|  | ||||
|     const loadOrJoinBoard = useCallback(async (userId: string, boardTeamId: string, boardId: string) => { | ||||
|         // and fetch its data | ||||
|         const result: any = await dispatch(loadBoardData(boardId)) | ||||
|         if (result.payload.blocks.length === 0 && userId) { | ||||
|             const member = await octoClient.joinBoard(boardId) | ||||
|             if (!member) { | ||||
|                 UserSettings.setLastBoardID(boardTeamId, null) | ||||
|                 UserSettings.setLastViewId(boardId, null) | ||||
|                 dispatch(setGlobalError('board-not-found')) | ||||
|     const onConfirmJoin = async () => { | ||||
|         if (me) { | ||||
|             joinBoard(me, teamId, match.params.boardId, true) | ||||
|             setShowJoinBoardDialog(false) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const joinBoard = async (myUser: IUser, boardTeamId: string, boardId: string, allowAdmin: boolean) => { | ||||
|         const member = await octoClient.joinBoard(boardId, allowAdmin) | ||||
|         if (!member) { | ||||
|             if (myUser.permissions?.find((s) => s === 'manage_system' || s === 'manage_team')) { | ||||
|                 setShowJoinBoardDialog(true) | ||||
|                 return | ||||
|             } | ||||
|             await dispatch(loadBoardData(boardId)) | ||||
|             UserSettings.setLastBoardID(boardTeamId, null) | ||||
|             UserSettings.setLastViewId(boardId, null) | ||||
|             dispatch(setGlobalError('board-not-found')) | ||||
|             return | ||||
|         } | ||||
|         const result: any = await dispatch(loadBoardData(boardId)) | ||||
|         if (result.payload.blocks.length > 0 && myUser.id) { | ||||
|             // set board as most recently viewed board | ||||
|             UserSettings.setLastBoardID(boardTeamId, boardId) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const loadOrJoinBoard = useCallback(async (myUser: IUser, boardTeamId: string, boardId: string) => { | ||||
|         // and fetch its data | ||||
|         const result: any = await dispatch(loadBoardData(boardId)) | ||||
|         if (result.payload.blocks.length === 0 && myUser.id) { | ||||
|             joinBoard(myUser, boardTeamId, boardId, false) | ||||
|         } else { | ||||
|             // set board as most recently viewed board | ||||
|             UserSettings.setLastBoardID(boardTeamId, boardId) | ||||
|         } | ||||
|  | ||||
|         dispatch(fetchBoardMembers({ | ||||
| @@ -204,9 +229,6 @@ const BoardPage = (props: Props): JSX.Element => { | ||||
|             // set the active board | ||||
|             dispatch(setCurrentBoard(match.params.boardId)) | ||||
|  | ||||
|             // and set it as most recently viewed board | ||||
|             UserSettings.setLastBoardID(teamId, match.params.boardId) | ||||
|  | ||||
|             if (viewId !== Constants.globalTeamId) { | ||||
|                 // reset current, even if empty string | ||||
|                 dispatch(setCurrentView(viewId)) | ||||
| @@ -220,7 +242,7 @@ const BoardPage = (props: Props): JSX.Element => { | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (match.params.boardId && !props.readonly && me) { | ||||
|             loadOrJoinBoard(me.id, teamId, match.params.boardId) | ||||
|             loadOrJoinBoard(me, teamId, match.params.boardId) | ||||
|         } | ||||
|     }, [teamId, match.params.boardId, me?.id]) | ||||
|  | ||||
| @@ -251,49 +273,71 @@ const BoardPage = (props: Props): JSX.Element => { | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <div className='BoardPage'> | ||||
|             {!props.new && <TeamToBoardAndViewRedirect/>} | ||||
|             <BackwardCompatibilityQueryParamsRedirect/> | ||||
|             <SetWindowTitleAndIcon/> | ||||
|             <UndoRedoHotKeys/> | ||||
|             <WebsocketConnection/> | ||||
|             <CloudMessage/> | ||||
|             <VersionMessage/> | ||||
|         <> | ||||
|             {showJoinBoardDialog && | ||||
|                 <ConfirmationDialog | ||||
|                     dialogBox={{ | ||||
|                         heading: intl.formatMessage({id: 'boardPage.confirm-join-title', defaultMessage: 'Join private board'}), | ||||
|                         subText: intl.formatMessage({ | ||||
|                             id: 'boardPage.confirm-join-text', | ||||
|                             defaultMessage: 'You are about to join a private board without explicitly being added by the board admin. Are you sure you wish to join this private board?', | ||||
|                         }), | ||||
|                         confirmButtonText: intl.formatMessage({id: 'boardPage.confirm-join-button', defaultMessage: 'Join'}), | ||||
|                         destructive: true, //board.channelId !== '', | ||||
|  | ||||
|             {!mobileWarningClosed && | ||||
|                 <div className='mobileWarning'> | ||||
|                     <div> | ||||
|                         <FormattedMessage | ||||
|                             id='Error.mobileweb' | ||||
|                             defaultMessage='Mobile web support is currently in early beta. Not all functionality may be present.' | ||||
|                         onConfirm: onConfirmJoin, | ||||
|                         onClose: () => { | ||||
|                             setShowJoinBoardDialog(false) | ||||
|                             history.goBack() | ||||
|                         }, | ||||
|                     }} | ||||
|                 />} | ||||
|  | ||||
|             {!showJoinBoardDialog && | ||||
|                 <div className='BoardPage'> | ||||
|                     {!props.new && <TeamToBoardAndViewRedirect/>} | ||||
|                     <BackwardCompatibilityQueryParamsRedirect/> | ||||
|                     <SetWindowTitleAndIcon/> | ||||
|                     <UndoRedoHotKeys/> | ||||
|                     <WebsocketConnection/> | ||||
|                     <CloudMessage/> | ||||
|                     <VersionMessage/> | ||||
|  | ||||
|                     {!mobileWarningClosed && | ||||
|                         <div className='mobileWarning'> | ||||
|                             <div> | ||||
|                                 <FormattedMessage | ||||
|                                     id='Error.mobileweb' | ||||
|                                     defaultMessage='Mobile web support is currently in early beta. Not all functionality may be present.' | ||||
|                                 /> | ||||
|                             </div> | ||||
|                             <IconButton | ||||
|                                 onClick={() => { | ||||
|                                     UserSettings.mobileWarningClosed = true | ||||
|                                     setMobileWarningClosed(true) | ||||
|                                 }} | ||||
|                                 icon={<CloseIcon/>} | ||||
|                                 title='Close' | ||||
|                                 className='margin-right' | ||||
|                             /> | ||||
|                         </div>} | ||||
|  | ||||
|                     {props.readonly && activeBoardId === undefined && | ||||
|                         <div className='error'> | ||||
|                             {intl.formatMessage({id: 'BoardPage.syncFailed', defaultMessage: 'Board may be deleted or access revoked.'})} | ||||
|                         </div>} | ||||
|                     { | ||||
|  | ||||
|                         // Don't display Templates page | ||||
|                         // if readonly mode and no board defined. | ||||
|                         (!props.readonly || activeBoardId !== undefined) && | ||||
|                         <Workspace | ||||
|                             readonly={props.readonly || false} | ||||
|                         /> | ||||
|                     </div> | ||||
|                     <IconButton | ||||
|                         onClick={() => { | ||||
|                             UserSettings.mobileWarningClosed = true | ||||
|                             setMobileWarningClosed(true) | ||||
|                         }} | ||||
|                         icon={<CloseIcon/>} | ||||
|                         title='Close' | ||||
|                         className='margin-right' | ||||
|                     /> | ||||
|                 </div>} | ||||
|  | ||||
|             {props.readonly && activeBoardId === undefined && | ||||
|                 <div className='error'> | ||||
|                     {intl.formatMessage({id: 'BoardPage.syncFailed', defaultMessage: 'Board may be deleted or access revoked.'})} | ||||
|                 </div>} | ||||
|  | ||||
|             { | ||||
|  | ||||
|                 // Don't display Templates page | ||||
|                 // if readonly mode and no board defined. | ||||
|                 (!props.readonly || activeBoardId !== undefined) && | ||||
|                 <Workspace | ||||
|                     readonly={props.readonly || false} | ||||
|                 /> | ||||
|                     } | ||||
|                 </div> | ||||
|             } | ||||
|         </div> | ||||
|         </> | ||||
|     ) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -36,7 +36,7 @@ export const fetchBoardMembers = createAsyncThunk( | ||||
|         const users = [] as IUser[] | ||||
|         const userIDs = members.map((member) => member.userId) | ||||
|  | ||||
|         const usersData = await client.getUsersList(userIDs) | ||||
|         const usersData = await client.getTeamUsersList(userIDs, teamId) | ||||
|         users.push(...usersData) | ||||
|  | ||||
|         thunkAPI.dispatch(setBoardUsers(users)) | ||||
| @@ -85,9 +85,13 @@ export const updateMembersEnsuringBoardsAndUsers = createAsyncThunk( | ||||
|             if (boardUsers[m.userId]) { | ||||
|                 return | ||||
|             } | ||||
|             const user = await client.getUser(m.userId) | ||||
|             if (user) { | ||||
|                 thunkAPI.dispatch(addBoardUsers([user])) | ||||
|  | ||||
|             const board = await client.getBoard(m.boardId) | ||||
|             if (board) { | ||||
|                 const user = await client.getTeamUsersList([m.userId], board.teamId) | ||||
|                 if (user) { | ||||
|                     thunkAPI.dispatch(addBoardUsers(user)) | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|   | ||||
| @@ -14,6 +14,7 @@ interface IUser { | ||||
|     update_at: number | ||||
|     is_bot: boolean | ||||
|     is_guest: boolean | ||||
|     permissions?: string[] | ||||
|     roles: string | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,29 @@ | ||||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||
|  | ||||
| exports[`widgets/adminBadge should match the snapshot for Admin 1`] = ` | ||||
| <div> | ||||
|   <div | ||||
|     class="AdminBadge" | ||||
|   > | ||||
|     <div | ||||
|       class="AdminBadge__box" | ||||
|     > | ||||
|       Admin | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| `; | ||||
|  | ||||
| exports[`widgets/adminBadge should match the snapshot for TeamAdmin 1`] = ` | ||||
| <div> | ||||
|   <div | ||||
|     class="AdminBadge" | ||||
|   > | ||||
|     <div | ||||
|       class="AdminBadge__box" | ||||
|     > | ||||
|       Team Admin | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| `; | ||||
							
								
								
									
										16
									
								
								webapp/src/widgets/adminBadge/adminBadge.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								webapp/src/widgets/adminBadge/adminBadge.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| .AdminBadge { | ||||
|     display: inline-flex; | ||||
|     align-items: center; | ||||
|     margin: 0 10px 0 4px; | ||||
| } | ||||
|  | ||||
| .AdminBadge__box { | ||||
|     padding: 2px 4px; | ||||
|     border: 0; | ||||
|     background: rgba(var(--center-channel-color-rgb), 0.16); | ||||
|     border-radius: 2px; | ||||
|     font-family: inherit; | ||||
|     font-size: 10px; | ||||
|     font-weight: 600; | ||||
|     line-height: 14px; | ||||
| } | ||||
							
								
								
									
										32
									
								
								webapp/src/widgets/adminBadge/adminBadge.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								webapp/src/widgets/adminBadge/adminBadge.test.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||
| // See LICENSE.txt for license information. | ||||
|  | ||||
| import React from 'react' | ||||
| import {render} from '@testing-library/react' | ||||
| import '@testing-library/jest-dom' | ||||
|  | ||||
| import {wrapIntl} from '../../testUtils' | ||||
|  | ||||
| import AdminBadge from './adminBadge' | ||||
|  | ||||
| describe('widgets/adminBadge', () => { | ||||
|     test('should match the snapshot for TeamAdmin', () => { | ||||
|         const {container} = render(wrapIntl(<AdminBadge permissions={['manage_team']}/>)) | ||||
|         expect(container).toMatchSnapshot() | ||||
|     }) | ||||
|  | ||||
|     test('should match the snapshot for Admin', () => { | ||||
|         const {container} = render(wrapIntl(<AdminBadge permissions={['manage_team', 'manage_system']}/>)) | ||||
|         expect(container).toMatchSnapshot() | ||||
|     }) | ||||
|  | ||||
|     test('should match the snapshot for empty', () => { | ||||
|         const {container} = render(wrapIntl(<AdminBadge permissions={[]}/>)) | ||||
|         expect(container).toMatchInlineSnapshot('<div />') | ||||
|     }) | ||||
|  | ||||
|     test('should match the snapshot for invalid permission', () => { | ||||
|         const {container} = render(wrapIntl(<AdminBadge permissions={['invalid_permission']}/>)) | ||||
|         expect(container).toMatchInlineSnapshot('<div />') | ||||
|     }) | ||||
| }) | ||||
							
								
								
									
										36
									
								
								webapp/src/widgets/adminBadge/adminBadge.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								webapp/src/widgets/adminBadge/adminBadge.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||
| // See LICENSE.txt for license information. | ||||
|  | ||||
| import React, {memo} from 'react' | ||||
| import {useIntl} from 'react-intl' | ||||
|  | ||||
| import './adminBadge.scss' | ||||
|  | ||||
| type Props = { | ||||
|     permissions?: string[] | ||||
| } | ||||
|  | ||||
| const AdminBadge = (props: Props) => { | ||||
|     const intl = useIntl() | ||||
|  | ||||
|     if (!props.permissions) { | ||||
|         return null | ||||
|     } | ||||
|     let text = '' | ||||
|     if (props.permissions?.find((s) => s === 'manage_system')) { | ||||
|         text = intl.formatMessage({id: 'AdminBadge.SystemAdmin', defaultMessage: 'Admin'}) | ||||
|     } else if (props.permissions?.find((s) => s === 'manage_team')) { | ||||
|         text = intl.formatMessage({id: 'AdminBadge.TeamAdmin', defaultMessage: 'Team Admin'}) | ||||
|     } else { | ||||
|         return null | ||||
|     } | ||||
|     return ( | ||||
|         <div className='AdminBadge'> | ||||
|             <div className='AdminBadge__box'> | ||||
|                 {text} | ||||
|             </div> | ||||
|         </div> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export default memo(AdminBadge) | ||||
		Reference in New Issue
	
	Block a user