mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-08 15:06:08 +02:00
Merge branch 'main' into rolling-stable
This commit is contained in:
commit
c35bbdb436
3
mattermost-plugin/server/manifest.go
generated
3
mattermost-plugin/server/manifest.go
generated
@ -45,7 +45,8 @@ const manifestStr = `
|
||||
"type": "bool",
|
||||
"help_text": "This allows board editors to share boards that can be accessed by anyone with the link.",
|
||||
"placeholder": "",
|
||||
"default": false
|
||||
"default": false,
|
||||
"hosting": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -109,6 +109,104 @@ exports[`components/rhsChannelBoards renders the RHS for channel boards 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/rhsChannelBoards renders the RHS for channel boards, no add 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoards"
|
||||
>
|
||||
<div
|
||||
class="rhs-boards-header"
|
||||
>
|
||||
<span
|
||||
class="linked-boards"
|
||||
>
|
||||
Linked boards
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="rhs-boards-list"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoardItem"
|
||||
>
|
||||
<div
|
||||
class="board-info"
|
||||
>
|
||||
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Untitled board
|
||||
</span>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
/>
|
||||
<div
|
||||
class="date"
|
||||
>
|
||||
Last update at: July 08, 2022, 8:10 PM
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="RHSChannelBoardItem"
|
||||
>
|
||||
<div
|
||||
class="board-info"
|
||||
>
|
||||
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Untitled board
|
||||
</span>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
/>
|
||||
<div
|
||||
class="date"
|
||||
>
|
||||
Last update at: July 08, 2022, 8:10 PM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/rhsChannelBoards renders with empty list of boards 1`] = `
|
||||
<div>
|
||||
<div
|
||||
@ -144,3 +242,31 @@ exports[`components/rhsChannelBoards renders with empty list of boards 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/rhsChannelBoards renders with empty list of boards, cannot add 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoards empty"
|
||||
>
|
||||
<h2>
|
||||
No boards are linked to Channel Name yet
|
||||
</h2>
|
||||
<div
|
||||
class="empty-paragraph"
|
||||
>
|
||||
Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view.
|
||||
</div>
|
||||
<div
|
||||
class="boards-screenshots"
|
||||
>
|
||||
<img
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {act, render} from '@testing-library/react'
|
||||
import {act, render, screen} from '@testing-library/react'
|
||||
import {mocked} from 'jest-mock'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
@ -44,6 +44,7 @@ describe('components/rhsChannelBoards', () => {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user-id',
|
||||
permissions: ['create_post']
|
||||
},
|
||||
},
|
||||
language: {
|
||||
@ -89,7 +90,8 @@ describe('components/rhsChannelBoards', () => {
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Add')
|
||||
expect(buttonElement).not.toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@ -107,6 +109,45 @@ describe('components/rhsChannelBoards', () => {
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Link boards to Channel Name')
|
||||
expect(buttonElement).not.toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders the RHS for channel boards, no add', async () => {
|
||||
const localState = {...state, users: {me:{id: 'user-id'}}}
|
||||
const store = mockStateStore([thunk], localState)
|
||||
let container: Element | DocumentFragment | null = null
|
||||
await act(async () => {
|
||||
const result = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoards/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Add')
|
||||
expect(buttonElement).toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with empty list of boards, cannot add', async () => {
|
||||
const localState = {...state, users: {me:{id: 'user-id'}}, boards: {...state.boards, boards: {}}}
|
||||
const store = mockStateStore([thunk], localState)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
await act(async () => {
|
||||
const result = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoards/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Link boards to Channel Name')
|
||||
expect(buttonElement).toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
@ -49,7 +49,7 @@ const RHSChannelBoards = () => {
|
||||
dispatch(loadMyBoardsMemberships()),
|
||||
dispatch(fetchMe()),
|
||||
]).then(() => setDataLoaded(true))
|
||||
}, [])
|
||||
}, [currentChannel?.id])
|
||||
|
||||
useWebsockets(teamId || '', (wsClient: WSClient) => {
|
||||
const onChangeBoardHandler = (_: WSClient, boards: Board[]): void => {
|
||||
@ -117,17 +117,19 @@ const RHSChannelBoards = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className='boards-screenshots'><img src={Utils.buildURL(boardsScreenshots, true)}/></div>
|
||||
<Button
|
||||
onClick={() => dispatch(setLinkToChannel(currentChannel.id))}
|
||||
emphasis='primary'
|
||||
size='medium'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='rhs-boards.link-boards-to-channel'
|
||||
defaultMessage='Link boards to {channelName}'
|
||||
values={{channelName: channelName}}
|
||||
/>
|
||||
</Button>
|
||||
{me?.permissions?.find((s) => s === 'create_post') &&
|
||||
<Button
|
||||
onClick={() => dispatch(setLinkToChannel(currentChannel.id))}
|
||||
emphasis='primary'
|
||||
size='medium'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='rhs-boards.link-boards-to-channel'
|
||||
defaultMessage='Link boards to {channelName}'
|
||||
values={{channelName: channelName}}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -143,16 +145,18 @@ const RHSChannelBoards = () => {
|
||||
defaultMessage='Linked boards'
|
||||
/>
|
||||
</span>
|
||||
<Button
|
||||
onClick={() => dispatch(setLinkToChannel(currentChannel.id))}
|
||||
icon={<AddIcon/>}
|
||||
emphasis='primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='rhs-boards.add'
|
||||
defaultMessage='Add'
|
||||
/>
|
||||
</Button>
|
||||
{me?.permissions?.find((s) => s === 'create_post') &&
|
||||
<Button
|
||||
onClick={() => dispatch(setLinkToChannel(currentChannel.id))}
|
||||
icon={<AddIcon/>}
|
||||
emphasis='primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='rhs-boards.add'
|
||||
defaultMessage='Add'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
<div className='rhs-boards-list'>
|
||||
{channelBoards.map((b) => (
|
||||
|
@ -249,6 +249,7 @@ export default class Plugin {
|
||||
if (lastViewedChannel !== currentChannel && currentChannel) {
|
||||
localStorage.setItem('focalboardLastViewedChannel:' + currentUserId, currentChannel)
|
||||
lastViewedChannel = currentChannel
|
||||
octoClient.channelId = currentChannel
|
||||
const currentChannelObj = mmStore.getState().entities.channels.channels[lastViewedChannel]
|
||||
store.dispatch(setChannel(currentChannelObj))
|
||||
}
|
||||
|
@ -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,17 @@ 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
|
||||
// - name: channelID
|
||||
// in: path
|
||||
// description: Channel ID
|
||||
// required: false
|
||||
// type: string
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
@ -118,6 +129,9 @@ 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")
|
||||
channelID := query.Get("channelID")
|
||||
|
||||
userID := getUserID(r)
|
||||
|
||||
@ -146,6 +160,16 @@ 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)
|
||||
}
|
||||
if channelID != "" && a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) {
|
||||
user.Permissions = append(user.Permissions, model.PermissionCreatePost.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) {
|
||||
@ -50,7 +63,18 @@ func (a *App) CanSeeUser(seerUser string, seenUser string) (bool, error) {
|
||||
}
|
||||
|
||||
func (a *App) SearchUserChannels(teamID string, userID string, query string) ([]*mmModel.Channel, error) {
|
||||
return a.store.SearchUserChannels(teamID, userID, query)
|
||||
channels, err := a.store.SearchUserChannels(teamID, userID, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var writeableChannels []*mmModel.Channel
|
||||
for _, channel := range channels {
|
||||
if a.permissions.HasPermissionToChannel(userID, channel.Id, model.PermissionCreatePost) {
|
||||
writeableChannels = append(writeableChannels, channel)
|
||||
}
|
||||
}
|
||||
return writeableChannels, nil
|
||||
}
|
||||
|
||||
func (a *App) GetChannel(teamID string, channelID string) (*mmModel.Channel, error) {
|
||||
|
85
server/app/user_test.go
Normal file
85
server/app/user_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/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)
|
||||
})
|
||||
|
||||
t.Run("test user channels", func(t *testing.T) {
|
||||
channelID := "Channel1"
|
||||
th.Store.EXPECT().SearchUserChannels(teamID, userID, "").Return([]*mmModel.Channel{{Id: channelID}}, nil)
|
||||
th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(true).Times(1)
|
||||
|
||||
channels, err := th.App.SearchUserChannels(teamID, userID, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(channels))
|
||||
})
|
||||
|
||||
t.Run("test user channels- no permissions", func(t *testing.T) {
|
||||
channelID := "Channel1"
|
||||
th.Store.EXPECT().SearchUserChannels(teamID, userID, "").Return([]*mmModel.Channel{{Id: channelID}}, nil)
|
||||
th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1)
|
||||
|
||||
channels, err := th.App.SearchUserChannels(teamID, userID, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(channels))
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
||||
@ -88,7 +91,7 @@ func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string
|
||||
}
|
||||
|
||||
func (*FakePermissionPluginAPI) HasPermissionToChannel(userID string, channelID string, permission *mmModel.Permission) bool {
|
||||
return channelID == "valid-channel-id"
|
||||
return channelID == "valid-channel-id" || channelID == "valid-channel-id-2"
|
||||
}
|
||||
|
||||
func getTestConfig() (*config.Configuration, error) {
|
||||
|
@ -6,7 +6,10 @@ import (
|
||||
|
||||
var (
|
||||
PermissionViewTeam = mmModel.PermissionViewTeam
|
||||
PermissionManageTeam = mmModel.PermissionManageTeam
|
||||
PermissionManageSystem = mmModel.PermissionManageSystem
|
||||
PermissionReadChannel = mmModel.PermissionReadChannel
|
||||
PermissionCreatePost = mmModel.PermissionCreatePost
|
||||
PermissionViewMembers = mmModel.PermissionViewMembers
|
||||
PermissionCreatePublicChannel = mmModel.PermissionCreatePublicChannel
|
||||
PermissionCreatePrivateChannel = mmModel.PermissionCreatePrivateChannel
|
||||
|
@ -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"`
|
||||
}
|
||||
|
||||
|
@ -344,7 +344,7 @@ func appendContentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Dif
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("appendContentChanges",
|
||||
logger.Trace("appendContentChanges",
|
||||
mlog.String("type", string(child.BlockType)),
|
||||
mlog.String("opString", opString),
|
||||
mlog.String("oldTitle", oldTitle),
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -119,9 +120,11 @@ func (cn *CallbackQueue) exec(f CallbackFunc) {
|
||||
// don't let a panic in the callback exit the thread.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stack := debug.Stack()
|
||||
cn.logger.Error("CallbackQueue callback panic",
|
||||
mlog.String("name", cn.name),
|
||||
mlog.Any("panic", r),
|
||||
mlog.String("stack", string(stack)),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
@ -454,7 +454,7 @@ func (pa *PluginAdapter) sendBoardMessage(teamID, boardID string, payload map[st
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastBlockChange(teamID string, block *model.Block) {
|
||||
pa.logger.Debug("BroadcastingBlockChange",
|
||||
pa.logger.Trace("BroadcastingBlockChange",
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", block.BoardID),
|
||||
mlog.String("blockID", block.ID),
|
||||
|
@ -41,16 +41,16 @@
|
||||
"BoardsUnfurl.Updated": "Aktulaizirano {time}",
|
||||
"Calculations.Options.average.displayName": "Prosjek",
|
||||
"Calculations.Options.average.label": "Prosjek",
|
||||
"Calculations.Options.count.displayName": "Zbroj",
|
||||
"Calculations.Options.count.label": "Zbroj",
|
||||
"Calculations.Options.countChecked.displayName": "Provjereno",
|
||||
"Calculations.Options.countChecked.label": "Zbroj provjeren",
|
||||
"Calculations.Options.countUnchecked.displayName": "Neprovjereno",
|
||||
"Calculations.Options.countUnchecked.label": "Zbroj neprovjeren",
|
||||
"Calculations.Options.count.displayName": "Broji",
|
||||
"Calculations.Options.count.label": "Broji",
|
||||
"Calculations.Options.countChecked.displayName": "Označeno",
|
||||
"Calculations.Options.countChecked.label": "Broji označene",
|
||||
"Calculations.Options.countUnchecked.displayName": "Neoznačeno",
|
||||
"Calculations.Options.countUnchecked.label": "Broji neoznačene",
|
||||
"Calculations.Options.countUniqueValue.displayName": "Jedinstveno",
|
||||
"Calculations.Options.countUniqueValue.label": "Broji jedinstvene vrijednosti",
|
||||
"Calculations.Options.countValue.displayName": "Vrijednosti",
|
||||
"Calculations.Options.countValue.label": "Vrijednost zbroja",
|
||||
"Calculations.Options.countValue.label": "Broji vrijednost",
|
||||
"Calculations.Options.dateRange.displayName": "Raspon",
|
||||
"Calculations.Options.dateRange.label": "Raspon",
|
||||
"Calculations.Options.earliest.displayName": "Najraniji",
|
||||
|
12
webapp/i18n/pt.json
Normal file
12
webapp/i18n/pt.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"AppBar.Tooltip": "Alternar quadros vinculados",
|
||||
"Attachment.Attachment-title": "Anexo",
|
||||
"AttachmentBlock.DeleteAction": "Apagar",
|
||||
"AttachmentBlock.addElement": "Adicionar {tipo}",
|
||||
"AttachmentBlock.delete": "Anexo apagado.",
|
||||
"AttachmentBlock.failed": "Este arquivo não pôde ser carregado pois ultrapassou o tamanho limite.",
|
||||
"AttachmentBlock.upload": "Carregando anexo.",
|
||||
"AttachmentBlock.uploadSuccess": "Anexo carregado.",
|
||||
"AttachmentElement.delete-confirmation-dialog-button-text": "Apagar",
|
||||
"AttachmentElement.download": "Baixar"
|
||||
}
|
@ -277,7 +277,7 @@
|
||||
"SidebarTour.SidebarCategories.Body": "Alla dina boards är nu organiserade i ditt nya sidofält. Du behöver inte längre växla mellan olika arbetsområden. Anpassade kategorier baserade på dina tidigare arbetsytor kan ha skapats automatiskt för dig som en del av din uppgradering av v7.2. Dessa kan tas bort eller redigeras enligt dina önskemål.",
|
||||
"SidebarTour.SidebarCategories.Link": "Mer information",
|
||||
"SidebarTour.SidebarCategories.Title": "Kategorier i sidoomenyn",
|
||||
"SiteStats.total_boards": "Totalt antal boards",
|
||||
"SiteStats.total_boards": "Totalt antal tavlor",
|
||||
"SiteStats.total_cards": "Totalt antal kort",
|
||||
"TableComponent.add-icon": "Lägg till ikon",
|
||||
"TableComponent.name": "Namn",
|
||||
|
@ -125,9 +125,113 @@
|
||||
"ConfirmationDialog.confirm-action": "Підтвердити",
|
||||
"ContentBlock.Delete": "Видалити",
|
||||
"ContentBlock.DeleteAction": "видалити",
|
||||
"ContentBlock.addElement": "додати {type}",
|
||||
"ContentBlock.checkbox": "прапорець",
|
||||
"ContentBlock.divider": "роздільник",
|
||||
"ContentBlock.editCardCheckbox": "позначений прапорець",
|
||||
"ContentBlock.editCardCheckboxText": "редагувати текст картки",
|
||||
"ContentBlock.editCardText": "редагувати текст картки",
|
||||
"ContentBlock.editText": "Редагувати текст...",
|
||||
"ContentBlock.image": "зображення",
|
||||
"ContentBlock.insertAbove": "Вставте вище",
|
||||
"ContentBlock.moveBlock": "перемістити вміст картки",
|
||||
"ContentBlock.moveDown": "Опустити",
|
||||
"ContentBlock.moveUp": "Підняти",
|
||||
"ContentBlock.text": "текст",
|
||||
"DateRange.clear": "Очистити",
|
||||
"DateRange.empty": "Пусто",
|
||||
"DateRange.endDate": "Дата закінчення",
|
||||
"DateRange.today": "Сьогодні",
|
||||
"DeleteBoardDialog.confirm-cancel": "Скасувати",
|
||||
"DeleteBoardDialog.confirm-delete": "Видалити",
|
||||
"DeleteBoardDialog.confirm-info": "Ви впевнені, що хочете видалити дошку “{boardTitle}”? Видалення призведе до видалення всіх карток на дошці.",
|
||||
"DeleteBoardDialog.confirm-info-template": "Ви впевнені, що хочете видалити шаблон дошки «{boardTitle}»?",
|
||||
"DeleteBoardDialog.confirm-tite": "Підтвердьте видалення дошки",
|
||||
"DeleteBoardDialog.confirm-tite-template": "Підтвердьте видалення шаблону дошки",
|
||||
"Dialog.closeDialog": "Закрити діалогове вікно",
|
||||
"EditableDayPicker.today": "Сьогодні",
|
||||
"Error.mobileweb": "Мобільна веб-підтримка зараз знаходиться на ранній стадії бета-тестування. Не всі функції можуть бути присутніми.",
|
||||
"Error.websocket-closed": "З'єднання через веб-сокет закрито, з'єднання перервано. Якщо це продовжується й далі, перевірте конфігурацію сервера або веб-проксі.",
|
||||
"Filter.contains": "містить",
|
||||
"Filter.ends-with": "закінчується на",
|
||||
"Filter.includes": "включає в себе",
|
||||
"Filter.is": "є",
|
||||
"Filter.is-empty": "пусто",
|
||||
"Filter.is-not-empty": "не порожній",
|
||||
"Filter.is-not-set": "не встановлено",
|
||||
"Filter.is-set": "встановлено",
|
||||
"Filter.not-contains": "не містить",
|
||||
"Filter.not-ends-with": "не закінчується",
|
||||
"Filter.not-includes": "не включає",
|
||||
"Filter.not-starts-with": "не починається з",
|
||||
"Filter.starts-with": "починається з",
|
||||
"FilterByText.placeholder": "фільтрувати текст",
|
||||
"FilterComponent.add-filter": "+ Додати фільтр",
|
||||
"FilterComponent.delete": "Видалити",
|
||||
"FilterValue.empty": "(порожній)",
|
||||
"FindBoardsDialog.IntroText": "Пошук дощок",
|
||||
"FindBoardsDialog.NoResultsFor": "Немає результатів для \"{searchQuery}\"",
|
||||
"FindBoardsDialog.NoResultsSubtext": "Перевірте правильність написання або спробуйте інший запит.",
|
||||
"FindBoardsDialog.SubTitle": "Введіть, щоб знайти дошку. Використовуйте <b>ВГОРУ/ВНИЗ</b> для перегляду. <b>ENTER</b>, щоб вибрати, <b>ESC</b>, щоб закрити",
|
||||
"FindBoardsDialog.Title": "Знайти дошки",
|
||||
"GroupBy.hideEmptyGroups": "Сховати {count} порожні групи",
|
||||
"GroupBy.showHiddenGroups": "Показати {count} прихованих груп",
|
||||
"GroupBy.ungroup": "Розгрупувати",
|
||||
"HideBoard.MenuOption": "Сховати дошку",
|
||||
"KanbanCard.untitled": "Без назви",
|
||||
"MentionSuggestion.is-not-board-member": "(не член правління)",
|
||||
"Mutator.new-board-from-template": "нова дошка з шаблону",
|
||||
"Mutator.new-card-from-template": "нова картка із шаблону",
|
||||
"Mutator.new-template-from-card": "новий шаблон із картки",
|
||||
"OnboardingTour.AddComments.Body": "Ви можете коментувати проблеми та навіть @згадувати інших користувачів Mattermost, щоб привернути їх увагу.",
|
||||
"OnboardingTour.AddComments.Title": "Додати коментарі",
|
||||
"OnboardingTour.AddDescription.Body": "Додайте опис до картки, щоб ваші товариші по команді знали, про що йде мова.",
|
||||
"OnboardingTour.AddDescription.Title": "Додайте опис",
|
||||
"OnboardingTour.AddProperties.Body": "Додайте карткам різні властивості, щоб зробити їх потужнішими.",
|
||||
"OnboardingTour.AddProperties.Title": "Додайте властивості",
|
||||
"OnboardingTour.AddView.Body": "Перейдіть сюди, щоб створити новий вид для організації дошки за допомогою різних макетів.",
|
||||
"OnboardingTour.AddView.Title": "Додайте новий вид",
|
||||
"OnboardingTour.CopyLink.Body": "Ви можете поділитися своїми картками з товаришами по команді, скопіювавши посилання та вставивши його в канал, пряме або групове повідомлення.",
|
||||
"OnboardingTour.CopyLink.Title": "Копіювати посилання",
|
||||
"OnboardingTour.OpenACard.Body": "Відкрийте картку, щоб дослідити потужні способи, за допомогою яких дошки можуть допомогти вам організувати вашу роботу.",
|
||||
"OnboardingTour.OpenACard.Title": "Відкрити картку",
|
||||
"OnboardingTour.ShareBoard.Body": "Ви можете поділитися своєю дошкою всередині, у своїй команді або опублікувати її публічно для видимості за межами вашої організації.",
|
||||
"OnboardingTour.ShareBoard.Title": "Поділитися дошкою",
|
||||
"PersonProperty.board-members": "Члени команди",
|
||||
"PersonProperty.me": "Я",
|
||||
"PersonProperty.non-board-members": "Не учасник команди",
|
||||
"PropertyMenu.Delete": "Видалити",
|
||||
"PropertyMenu.changeType": "Змінити тип власності",
|
||||
"PropertyMenu.selectType": "Виберіть тип властивості",
|
||||
"PropertyMenu.typeTitle": "Тип",
|
||||
"PropertyType.Checkbox": "Прапорець",
|
||||
"PropertyType.CreatedBy": "Створений",
|
||||
"PropertyType.CreatedTime": "Час створення",
|
||||
"PropertyType.Date": "Дата",
|
||||
"PropertyType.Email": "Email",
|
||||
"PropertyType.MultiPerson": "Кілька осіб",
|
||||
"PropertyType.MultiSelect": "Множинний вибір",
|
||||
"PropertyType.Number": "Номер",
|
||||
"PropertyType.Person": "Особа",
|
||||
"PropertyType.Phone": "Телефон",
|
||||
"PropertyType.Select": "Обрати",
|
||||
"PropertyType.Text": "Текст",
|
||||
"PropertyType.Unknown": "Невідомий",
|
||||
"PropertyType.UpdatedBy": "Оновлено користувачем",
|
||||
"PropertyType.UpdatedTime": "Час останнього оновлення",
|
||||
"PropertyType.Url": "URL",
|
||||
"PropertyValueElement.empty": "Пусто",
|
||||
"RegistrationLink.confirmRegenerateToken": "Це призведе до скасування попередніх спільних посилань. Продовжити?",
|
||||
"RegistrationLink.copiedLink": "Скопійовано!",
|
||||
"RegistrationLink.copyLink": "Копіювати посилання",
|
||||
"RegistrationLink.description": "Поділіться цим посиланням, щоб інші могли створити облікові записи:",
|
||||
"RegistrationLink.regenerateToken": "Згенерувати новий токен",
|
||||
"RegistrationLink.tokenRegenerated": "Реєстраційне посилання відновлено",
|
||||
"ShareBoard.PublishDescription": "Опублікуйте та поділіться посиланням лише для читання з усіма в Інтернеті.",
|
||||
"ShareBoard.PublishTitle": "Опублікувати в Інтернеті",
|
||||
"ShareBoard.ShareInternal": "Поділитися всередині організації",
|
||||
"ShareBoard.ShareInternalDescription": "Користувачі, які мають дозволи, зможуть використовувати це посилання.",
|
||||
"ShareBoard.Title": "Поділиться Дошкою",
|
||||
"Sidebar.delete-board": "Видалити дошку",
|
||||
"SidebarCategories.CategoryMenu.Delete": "Видалити категорію",
|
||||
"SidebarCategories.CategoryMenu.DeleteModal.Title": "Видалити дану категорію?",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"Attachment.Attachment-title": "附件",
|
||||
"AttachmentBlock.DeleteAction": "刪除",
|
||||
"AttachmentBlock.addElement": "添加 {type}",
|
||||
"AttachmentBlock.delete": "附件刪除成功。",
|
||||
"AttachmentBlock.delete": "已刪除附件",
|
||||
"AttachmentBlock.failed": "無法上傳文件。 附件大小已達到限制。",
|
||||
"AttachmentBlock.upload": "附件正在上傳。",
|
||||
"AttachmentBlock.uploadSuccess": "附件已上傳",
|
||||
@ -88,7 +88,7 @@
|
||||
"CardDetail.add-icon": "新增圖示",
|
||||
"CardDetail.add-property": "+ 新增屬性",
|
||||
"CardDetail.addCardText": "新增卡片文本",
|
||||
"CardDetail.limited-body": "升級到專業版或是企業計劃,以查看封存卡片,獲得無限看版,無限卡片和更多功能。",
|
||||
"CardDetail.limited-body": "升級到專業版或是企業版",
|
||||
"CardDetail.limited-button": "升級",
|
||||
"CardDetail.limited-title": "此卡片被影藏",
|
||||
"CardDetail.moveContent": "移動卡片內容",
|
||||
@ -103,9 +103,9 @@
|
||||
"CardDetailProperty.property-deleted": "成功刪除 {propertyName}!",
|
||||
"CardDetailProperty.property-name-change-subtext": "類型從 \"{oldPropType}\" 變更為 \"{newPropType}\"",
|
||||
"CardDetial.limited-link": "了解更多我們的計畫.",
|
||||
"CardDialog.delete-confirmation-dialog-attachment": "確認刪除附件!",
|
||||
"CardDialog.delete-confirmation-dialog-attachment": "確認刪除附件",
|
||||
"CardDialog.delete-confirmation-dialog-button-text": "刪除",
|
||||
"CardDialog.delete-confirmation-dialog-heading": "確認刪除卡片!",
|
||||
"CardDialog.delete-confirmation-dialog-heading": "確認刪除卡片",
|
||||
"CardDialog.editing-template": "您正在編輯範本。",
|
||||
"CardDialog.nocard": "卡片不存在或者無法被存取。",
|
||||
"Categories.CreateCategoryDialog.CancelText": "取消",
|
||||
@ -114,10 +114,13 @@
|
||||
"Categories.CreateCategoryDialog.UpdateText": "更新",
|
||||
"CenterPanel.Login": "登入",
|
||||
"CenterPanel.Share": "分享",
|
||||
"ChannelIntro.CreateBoard": "建立看板",
|
||||
"CloudMessage.cloud-server": "獲得免費的雲端伺服器.",
|
||||
"ColorOption.selectColor": "{color} 選擇顏色",
|
||||
"Comment.delete": "刪除",
|
||||
"CommentsList.send": "發送",
|
||||
"ConfirmPerson.empty": "空白",
|
||||
"ConfirmPerson.search": "查詢...",
|
||||
"ConfirmationDialog.cancel-action": "取消",
|
||||
"ConfirmationDialog.confirm-action": "確認",
|
||||
"ContentBlock.Delete": "刪除",
|
||||
@ -165,6 +168,7 @@
|
||||
"FilterByText.placeholder": "過濾文字",
|
||||
"FilterComponent.add-filter": "+ 增加過濾條件",
|
||||
"FilterComponent.delete": "刪除",
|
||||
"FilterValue.empty": "(空白)",
|
||||
"FindBoardsDialog.IntroText": "查詢看板",
|
||||
"FindBoardsDialog.NoResultsFor": "「{searchQuery}」搜尋未果",
|
||||
"FindBoardsDialog.NoResultsSubtext": "檢查錯字或嘗試其他搜尋.",
|
||||
@ -183,7 +187,7 @@
|
||||
"OnboardingTour.AddComments.Title": "新增評論",
|
||||
"OnboardingTour.AddDescription.Body": "在卡片上新增描述讓其他成員知道此卡片內容.",
|
||||
"OnboardingTour.AddDescription.Title": "新增敘述",
|
||||
"OnboardingTour.AddProperties.Body": "為卡片新增各式屬性使其更加強大!",
|
||||
"OnboardingTour.AddProperties.Body": "為卡片新增各式屬性使其更加強大",
|
||||
"OnboardingTour.AddProperties.Title": "新增屬性",
|
||||
"OnboardingTour.AddView.Body": "轉到此處創建一個新視圖以使用不同的佈局組織您的看板。",
|
||||
"OnboardingTour.AddView.Title": "新增視圖",
|
||||
@ -193,7 +197,8 @@
|
||||
"OnboardingTour.OpenACard.Title": "瀏覽卡片",
|
||||
"OnboardingTour.ShareBoard.Body": "您可以在內部、團隊內部分享看板,或公開發布讓組織外部查看。",
|
||||
"OnboardingTour.ShareBoard.Title": "分享看板",
|
||||
"PersonProperty.board-members": "看版成員",
|
||||
"PersonProperty.board-members": "看板成員",
|
||||
"PersonProperty.me": "我",
|
||||
"PersonProperty.non-board-members": "不是看板成員",
|
||||
"PropertyMenu.Delete": "刪除",
|
||||
"PropertyMenu.changeType": "修改屬性類型",
|
||||
@ -273,7 +278,7 @@
|
||||
"SidebarTour.SidebarCategories.Link": "更多",
|
||||
"SidebarTour.SidebarCategories.Title": "邊欄類別",
|
||||
"SiteStats.total_boards": "所有看板",
|
||||
"SiteStats.total_cards": "總牌數",
|
||||
"SiteStats.total_cards": "總卡片數",
|
||||
"TableComponent.add-icon": "加入圖示",
|
||||
"TableComponent.name": "姓名",
|
||||
"TableComponent.plus-new": "+ 新增",
|
||||
@ -284,6 +289,7 @@
|
||||
"TableHeaderMenu.insert-right": "在右側插入",
|
||||
"TableHeaderMenu.sort-ascending": "升序排列",
|
||||
"TableHeaderMenu.sort-descending": "降序排列",
|
||||
"TableRow.DuplicateCard": "複製卡片",
|
||||
"TableRow.MoreOption": "更多操作",
|
||||
"TableRow.open": "開啟",
|
||||
"TopBar.give-feedback": "提供回饋",
|
||||
@ -336,9 +342,9 @@
|
||||
"ViewLimitDialog.Heading": "已達到每個看板觀看限制",
|
||||
"ViewLimitDialog.PrimaryButton.Title.Admin": "升級",
|
||||
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "通知管理者",
|
||||
"ViewLimitDialog.Subtext.Admin": "升級到專業版或企業版,獲得每個看板無限瀏覽、無限卡片,以及更多。",
|
||||
"ViewLimitDialog.Subtext.Admin": "升級到專業版或企業版",
|
||||
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "了解更多我們的計畫。",
|
||||
"ViewLimitDialog.Subtext.RegularUser": "通知你的管理員升級到專業版或是企業版,獲得無限使用看板、卡片、更多。",
|
||||
"ViewLimitDialog.Subtext.RegularUser": "通知你的管理員升級到專業版或是企業版",
|
||||
"ViewLimitDialog.UpgradeImg.AltText": "升級圖片",
|
||||
"ViewLimitDialog.notifyAdmin.Success": "已經通知管理者",
|
||||
"ViewTitle.hide-description": "隱藏敘述",
|
||||
@ -355,17 +361,19 @@
|
||||
"Workspace.editing-board-template": "您正在編輯版面範本。",
|
||||
"badge.guest": "訪客",
|
||||
"boardSelector.confirm-link-board": "連結看板與頻道",
|
||||
"boardSelector.confirm-link-board-button": "是,連結看版",
|
||||
"boardSelector.confirm-link-board-button": "是,連結看板",
|
||||
"boardSelector.confirm-link-board-subtext": "當你將\"{boardName}\"連接到頻道時,該頻道的所有成員(現有的和新的)都可以編輯。並不包含訪客身分。你可以在任何時候從一個頻道上取消看板的連接。",
|
||||
"boardSelector.confirm-link-board-subtext-with-other-channel": "當你將\"{boardName}\"連接到頻道時,該頻道的所有成員(現有的和新的)都可以編輯。並不包含訪客身分。{lineBreak} 看板目前正連接到另一個頻道。如果选择在這裡連接它,將取消另一個連接。",
|
||||
"boardSelector.create-a-board": "建立看版",
|
||||
"boardSelector.create-a-board": "建立看板",
|
||||
"boardSelector.link": "連結",
|
||||
"boardSelector.search-for-boards": "搜尋看板",
|
||||
"boardSelector.title": "連結看版",
|
||||
"boardSelector.title": "連結看板",
|
||||
"boardSelector.unlink": "未連結",
|
||||
"calendar.month": "月份",
|
||||
"calendar.today": "今日",
|
||||
"calendar.week": "週別",
|
||||
"centerPanel.undefined": "沒有 {propertyName}",
|
||||
"centerPanel.unknown-user": "未知使用者",
|
||||
"cloudMessage.learn-more": "學習更多",
|
||||
"createImageBlock.failed": "無法上傳檔案,檔案大小超過限制。",
|
||||
"default-properties.badges": "評論和描述",
|
||||
@ -374,19 +382,23 @@
|
||||
"error.back-to-team": "回到團隊",
|
||||
"error.board-not-found": "沒有找到看板.",
|
||||
"error.go-login": "登入",
|
||||
"error.invalid-read-only-board": "沒有權限進入此看版.登入後才能訪問.",
|
||||
"error.invalid-read-only-board": "沒有權限進入此看板.登入後才能訪問.",
|
||||
"error.not-logged-in": "已被登出,請再次登入使用看板。",
|
||||
"error.page.title": "很抱歉,發生了些錯誤",
|
||||
"error.team-undefined": "不是有效的團隊。",
|
||||
"error.unknown": "發生一個錯誤。",
|
||||
"generic.previous": "上一篇",
|
||||
"guest-no-board.subtitle": "你尚未有權限進入此看板,請等人把你加入任何看板。",
|
||||
"guest-no-board.title": "尚未有看版",
|
||||
"guest-no-board.title": "尚未有看板",
|
||||
"imagePaste.upload-failed": "有些檔案無法上傳.檔案大小達上限",
|
||||
"limitedCard.title": "影藏卡片",
|
||||
"login.log-in-button": "登錄",
|
||||
"login.log-in-title": "登錄",
|
||||
"login.register-button": "或創建一個帳戶(如果您沒有帳戶)",
|
||||
"new_channel_modal.create_board.empty_board_description": "建立新的空白看板",
|
||||
"new_channel_modal.create_board.empty_board_title": "空白看板",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "選擇一個範本",
|
||||
"new_channel_modal.create_board.title": "在這個頻道新建一個看板",
|
||||
"notification-box-card-limit-reached.close-tooltip": "小睡十天",
|
||||
"notification-box-card-limit-reached.contact-link": "通知管理員",
|
||||
"notification-box-card-limit-reached.link": "升級到付費版",
|
||||
@ -412,8 +424,8 @@
|
||||
"rhs-boards.linked-boards": "連結看板",
|
||||
"rhs-boards.no-boards-linked-to-channel": "還沒有看板與{channelName}連接",
|
||||
"rhs-boards.no-boards-linked-to-channel-description": "看板是一個專案管理工具,可以使用熟悉的圖表幫助我們定義、組織、追蹤和管理跨團隊工作。",
|
||||
"rhs-boards.unlink-board": "未連結看版",
|
||||
"rhs-boards.unlink-board1": "未連結看版",
|
||||
"rhs-boards.unlink-board": "未連結看板",
|
||||
"rhs-boards.unlink-board1": "未連結看板",
|
||||
"rhs-channel-boards-header.title": "板塊",
|
||||
"share-board.publish": "發布",
|
||||
"share-board.share": "分享",
|
||||
|
@ -107,6 +107,7 @@ const BoardsSwitcher = (props: Props): JSX.Element => {
|
||||
inverted={true}
|
||||
className='add-board-icon'
|
||||
icon={<AddIcon/>}
|
||||
title={'Add Board Dropdown'}
|
||||
/>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
|
@ -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>
|
||||
|
@ -8,6 +8,7 @@ enum Permission {
|
||||
DeleteBoard = 'delete_board',
|
||||
ShareBoard = 'share_board',
|
||||
ManageBoardRoles = 'manage_board_roles',
|
||||
ChannelCreatePost = 'create_post',
|
||||
ManageBoardCards = 'manage_board_cards',
|
||||
ManageBoardProperties = 'manage_board_properties',
|
||||
CommentBoardCards = 'comment_board_cards',
|
||||
@ -196,6 +197,7 @@ class Constants {
|
||||
}
|
||||
|
||||
static readonly globalTeamId = '0'
|
||||
static readonly noChannelID = '0'
|
||||
|
||||
static readonly myInsights = 'MY'
|
||||
|
||||
|
@ -51,7 +51,7 @@ class OctoClient {
|
||||
localStorage.setItem('focalboardSessionId', value)
|
||||
}
|
||||
|
||||
constructor(serverUrl?: string, public teamId = Constants.globalTeamId) {
|
||||
constructor(serverUrl?: string, public teamId = Constants.globalTeamId, public channelId = Constants.noChannelID) {
|
||||
this.serverUrl = serverUrl
|
||||
}
|
||||
|
||||
@ -160,7 +160,22 @@ class OctoClient {
|
||||
}
|
||||
|
||||
async getMe(): Promise<IUser | undefined> {
|
||||
const path = '/api/v2/users/me'
|
||||
let path = '/api/v2/users/me'
|
||||
let parameters = ''
|
||||
if (this.teamId !== Constants.globalTeamId) {
|
||||
parameters = `teamID=${this.teamId}`
|
||||
}
|
||||
if (this.channelId !== Constants.noChannelID) {
|
||||
const channelClause = `channelID=${this.channelId}`
|
||||
if (parameters) {
|
||||
parameters += '&' + channelClause
|
||||
} else {
|
||||
parameters = channelClause
|
||||
}
|
||||
}
|
||||
if (parameters) {
|
||||
path += '?' + parameters
|
||||
}
|
||||
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
|
||||
if (response.status !== 200) {
|
||||
return undefined
|
||||
@ -467,12 +482,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 +698,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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -302,7 +302,7 @@ describe('properties/dateRange', () => {
|
||||
// About `Date()`
|
||||
// > "When called as a function, returns a string representation of the current date and time"
|
||||
const date = new Date()
|
||||
const today = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
const today = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12)
|
||||
|
||||
const {getByText, getByTitle} = render(component)
|
||||
const dayDisplay = getByText('Empty')
|
||||
|
@ -97,6 +97,7 @@ function DateRange(props: PropertyProps): JSX.Element {
|
||||
|
||||
const handleDayClick = (day: Date) => {
|
||||
const range: DateProperty = {}
|
||||
day.setHours(12)
|
||||
if (isRange) {
|
||||
const newRange = DateUtils.addDayToRange(day, {from: dateFrom, to: dateTo})
|
||||
range.from = newRange.from?.getTime()
|
||||
|
@ -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)
|
@ -7,6 +7,8 @@ import {Picker, BaseEmoji} from 'emoji-mart'
|
||||
|
||||
import './emojiPicker.scss'
|
||||
|
||||
import emojiSpirit from '../../static/emoji_spirit.png'
|
||||
|
||||
type Props = {
|
||||
onSelect: (emoji: string) => void
|
||||
}
|
||||
@ -18,6 +20,7 @@ const EmojiPicker: FC<Props> = (props: Props): JSX.Element => (
|
||||
>
|
||||
<Picker
|
||||
onSelect={(emoji: BaseEmoji) => props.onSelect(emoji.native)}
|
||||
backgroundImageFn={() => emojiSpirit}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
BIN
webapp/static/emoji_spirit.png
Normal file
BIN
webapp/static/emoji_spirit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 MiB |
Loading…
Reference in New Issue
Block a user