mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-24 13:43:12 +02:00
Fix for users adding board links to Read-only channels (#4581)
* initial commit for channel permissions * add test * check for write post when returning channels * lint fix * fix test --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
230e519352
commit
f88ee1ece3
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))
|
||||
}
|
||||
|
@ -113,6 +113,11 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
||||
// description: Team ID
|
||||
// required: false
|
||||
// type: string
|
||||
// - name: channelID
|
||||
// in: path
|
||||
// description: Channel ID
|
||||
// required: false
|
||||
// type: string
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
@ -126,6 +131,7 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
query := r.URL.Query()
|
||||
teamID := query.Get("teamID")
|
||||
channelID := query.Get("channelID")
|
||||
|
||||
userID := getUserID(r)
|
||||
|
||||
@ -160,6 +166,9 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
|
@ -63,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) {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -61,4 +62,24 @@ func TestSearchUsers(t *testing.T) {
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
@ -91,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) {
|
||||
|
@ -9,6 +9,7 @@ var (
|
||||
PermissionManageTeam = mmModel.PermissionManageTeam
|
||||
PermissionManageSystem = mmModel.PermissionManageSystem
|
||||
PermissionReadChannel = mmModel.PermissionReadChannel
|
||||
PermissionCreatePost = mmModel.PermissionCreatePost
|
||||
PermissionViewMembers = mmModel.PermissionViewMembers
|
||||
PermissionCreatePublicChannel = mmModel.PermissionCreatePublicChannel
|
||||
PermissionCreatePrivateChannel = mmModel.PermissionCreatePrivateChannel
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
@ -161,8 +161,20 @@ class OctoClient {
|
||||
|
||||
async getMe(): Promise<IUser | undefined> {
|
||||
let path = '/api/v2/users/me'
|
||||
let parameters = ''
|
||||
if (this.teamId !== Constants.globalTeamId) {
|
||||
path += `?teamID=${this.teamId}`
|
||||
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) {
|
||||
|
Loading…
Reference in New Issue
Block a user