mirror of
https://github.com/mattermost/focalboard.git
synced 2025-03-26 20:53:55 +02:00
Allow to confirm add users on mention or assign to person field (#3724)
* Allow to confirm add users on mention or assign to person field * Improving the confirm add user for notification modal style * Add confirmation add user modal tests * Fixing tests * Fixing styles * Adding missed snapshots file * Fixing tests * Fixing other tiny errors * Fixing tests * Fixing tests
This commit is contained in:
parent
ad3b8fd454
commit
95c69cc46b
@ -0,0 +1,164 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`/components/confirmAddUserForNotifications should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back confirmation-dialog-box"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
/>
|
||||
<div
|
||||
class="wrapper"
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="toolbar"
|
||||
>
|
||||
<button
|
||||
aria-label="Close dialog"
|
||||
class="IconButton dialog__close size--medium"
|
||||
title="Close dialog"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="toolbar--right"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="box-area"
|
||||
title="Confirmation Dialog Box"
|
||||
>
|
||||
<h3
|
||||
class="text-heading5"
|
||||
>
|
||||
Add fake-username to board
|
||||
</h3>
|
||||
<div
|
||||
class="sub-text"
|
||||
>
|
||||
<div
|
||||
class="ConfirmAddUserForNotifications"
|
||||
>
|
||||
<p>
|
||||
fake-username is not a member of the board, and will not received any notifications about it.
|
||||
</p>
|
||||
<p>
|
||||
Do you want to add fake-username to the board?
|
||||
</p>
|
||||
<div
|
||||
class="permissions-title"
|
||||
>
|
||||
<label>
|
||||
Permissions
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="select css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions text"
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
/>
|
||||
<div
|
||||
class=" css-1s2u09g-control"
|
||||
>
|
||||
<div
|
||||
class=" css-319lph-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class=" css-qc6sy-singleValue"
|
||||
>
|
||||
Editor
|
||||
</div>
|
||||
<div
|
||||
class=" css-6j8wv5-Input"
|
||||
data-value=""
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class=""
|
||||
id="react-select-2-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class=" css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<span
|
||||
class=" css-1okebmr-indicatorSeparator"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class=" css-tlfecz-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="action-buttons"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--tertiary size--medium"
|
||||
title="Cancel"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Cancel
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="Button filled size--medium"
|
||||
title="Add to board"
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
Add to board
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -10,18 +10,22 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import mutator from '../mutator'
|
||||
import {IUser} from '../user'
|
||||
import {Utils} from '../utils'
|
||||
import octoClient from '../octoClient'
|
||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||
import {mockDOM, mockStateStore, wrapDNDIntl} from '../testUtils'
|
||||
|
||||
import CardDialog from './cardDialog'
|
||||
|
||||
jest.mock('../mutator')
|
||||
jest.mock('../octoClient')
|
||||
jest.mock('../utils')
|
||||
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
|
||||
|
||||
const mockedUtils = mocked(Utils, true)
|
||||
const mockedMutator = mocked(mutator, true)
|
||||
const mockedOctoClient = mocked(octoClient, true)
|
||||
mockedUtils.createGuid.mockReturnValue('test-id')
|
||||
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
|
||||
|
||||
@ -84,6 +88,8 @@ describe('components/cardDialog', () => {
|
||||
blockSubscriptions: [],
|
||||
},
|
||||
}
|
||||
|
||||
mockedOctoClient.searchTeamUsers.mockResolvedValue(Object.values(state.users.boardUsers) as IUser[])
|
||||
const store = mockStateStore([], state)
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
@ -10,6 +10,8 @@ import {mockDOM, mockStateStore, wrapDNDIntl} from '../testUtils'
|
||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||
import {IPropertyTemplate} from '../blocks/board'
|
||||
import {Utils} from '../utils'
|
||||
import {IUser} from '../user'
|
||||
import octoClient from '../octoClient'
|
||||
import Mutator from '../mutator'
|
||||
import {Constants} from '../constants'
|
||||
|
||||
@ -26,11 +28,13 @@ jest.mock('react-router-dom', () => {
|
||||
}
|
||||
})
|
||||
jest.mock('../utils')
|
||||
jest.mock('../octoClient')
|
||||
jest.mock('../mutator')
|
||||
jest.mock('../telemetry/telemetryClient')
|
||||
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
|
||||
const mockedUtils = mocked(Utils, true)
|
||||
const mockedMutator = mocked(Mutator, true)
|
||||
const mockedOctoClient= mocked(octoClient, true)
|
||||
mockedUtils.createGuid.mockReturnValue('test-id')
|
||||
mockedUtils.generateClassName = jest.requireActual('../utils').Utils.generateClassName
|
||||
describe('components/centerPanel', () => {
|
||||
@ -139,6 +143,7 @@ describe('components/centerPanel', () => {
|
||||
},
|
||||
},
|
||||
}
|
||||
mockedOctoClient.searchTeamUsers.mockResolvedValue(Object.values(state.users.boardUsers) as IUser[])
|
||||
const store = mockStateStore([], state)
|
||||
beforeAll(() => {
|
||||
mockDOM()
|
||||
|
32
webapp/src/components/confirmAddUserForNotifications.scss
Normal file
32
webapp/src/components/confirmAddUserForNotifications.scss
Normal file
@ -0,0 +1,32 @@
|
||||
@import '../styles/z-index';
|
||||
|
||||
.ConfirmAddUserForNotifications {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.select {
|
||||
text-align: left;
|
||||
width: 250px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.permissions-title {
|
||||
width: 250px;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
left: 6px;
|
||||
height: 0;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
|
||||
label {
|
||||
@include z-index(modal-permission-label);
|
||||
position: absolute;
|
||||
padding: 0 5px;
|
||||
margin: 0;
|
||||
background-color: rgb(var(--center-channel-bg-rgb));
|
||||
color: rgba(var(--center-channel-color-rgb), 0.8);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import '@testing-library/jest-dom'
|
||||
import {render} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import React from 'react'
|
||||
|
||||
import {wrapDNDIntl} from '../testUtils'
|
||||
import {IUser} from '../user'
|
||||
|
||||
import ConfirmAddUserForNotifications from './confirmAddUserForNotifications'
|
||||
|
||||
describe('/components/confirmAddUserForNotifications', () => {
|
||||
it('should match snapshot', async () => {
|
||||
const result = render(
|
||||
wrapDNDIntl(
|
||||
<ConfirmAddUserForNotifications
|
||||
user={{id: 'fake-user-id', username: 'fake-username'} as IUser}
|
||||
onConfirm={jest.fn()}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
),
|
||||
)
|
||||
expect(result.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('confirm button click, run onConfirm Function once', () => {
|
||||
const onConfirm = jest.fn()
|
||||
|
||||
const result = render(
|
||||
wrapDNDIntl(
|
||||
<ConfirmAddUserForNotifications
|
||||
user={{id: 'fake-user-id', username: 'fake-username'} as IUser}
|
||||
onConfirm={onConfirm}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
),
|
||||
)
|
||||
userEvent.click(result.getByTitle('Add to board'))
|
||||
expect(onConfirm).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('cancel button click runs onClose function', () => {
|
||||
const onClose = jest.fn()
|
||||
|
||||
const result = render(
|
||||
wrapDNDIntl(
|
||||
<ConfirmAddUserForNotifications
|
||||
user={{id: 'fake-user-id', username: 'fake-username'} as IUser}
|
||||
onConfirm={jest.fn()}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
),
|
||||
)
|
||||
userEvent.click(result.getByTitle('Cancel'))
|
||||
expect(onClose).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
87
webapp/src/components/confirmAddUserForNotifications.tsx
Normal file
87
webapp/src/components/confirmAddUserForNotifications.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useRef} from 'react'
|
||||
import Select from 'react-select'
|
||||
import {useIntl, FormattedMessage} from 'react-intl'
|
||||
|
||||
import {IUser} from '../user'
|
||||
|
||||
import ConfirmationDialog from './confirmationDialogBox'
|
||||
|
||||
import './confirmAddUserForNotifications.scss'
|
||||
|
||||
type Props = {
|
||||
user: IUser,
|
||||
onConfirm: (userId: string, role: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const ConfirmAddUserForNotifications = (props: Props): JSX.Element => {
|
||||
const {user} = props
|
||||
const [newUserRole, setNewUserRole] = useState('Editor')
|
||||
const userRole = useRef<string>('Editor')
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const roleOptions = [
|
||||
{id: 'Admin', label: intl.formatMessage({id:'PersonProperty.add-user-admin-role', defaultMessage:'Admin'})},
|
||||
{id: 'Editor', label: intl.formatMessage({id:'PersonProperty.add-user-editor-role', defaultMessage:'Editor'})},
|
||||
{id: 'Commenter', label: intl.formatMessage({id:'PersonProperty.add-user-commenter-role', defaultMessage:'Commenter'})},
|
||||
{id: 'Viewer', label: intl.formatMessage({id:'PersonProperty.add-user-viewer-role', defaultMessage:'Viewer'})},
|
||||
]
|
||||
|
||||
const subText = (
|
||||
<div className='ConfirmAddUserForNotifications'>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='person.add-user-to-board-warning'
|
||||
defaultMessage='{username} is not a member of the board, and will not received any notifications about it.'
|
||||
values={{username: props.user.username}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='person.add-user-to-board-question'
|
||||
defaultMessage='Do you want to add {username} to the board?'
|
||||
values={{username: props.user.username}}
|
||||
/>
|
||||
</p>
|
||||
<div className='permissions-title'>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='person.add-user-to-board-permissions'
|
||||
defaultMessage='Permissions'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Select
|
||||
className='select'
|
||||
getOptionLabel={(o: {id: string, label: string}) => o.label}
|
||||
getOptionValue={(o: {id: string, label: string}) => o.id}
|
||||
styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }) }}
|
||||
menuPortalTarget={document.body}
|
||||
options={roleOptions}
|
||||
onChange={(option) => {
|
||||
setNewUserRole(option?.id || 'Editor')
|
||||
userRole.current = option?.id || 'Editor'
|
||||
}}
|
||||
value={roleOptions.find((o) => o.id === newUserRole)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
dialogBox={{
|
||||
heading: intl.formatMessage({id: 'person.add-user-to-board', defaultMessage: 'Add {username} to board'}, {username: props.user.username}),
|
||||
subText,
|
||||
confirmButtonText: intl.formatMessage({id: 'person.add-user-to-board-confirm-button', defaultMessage: 'Add to board'}),
|
||||
onConfirm: () => props.onConfirm(user.id, userRole.current),
|
||||
onClose: props.onClose,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmAddUserForNotifications
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {ReactElement} from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
import {EntryComponentProps} from '@draft-js-plugins/mention/lib/MentionSuggestions/Entry/Entry'
|
||||
|
||||
import GuestBadge from '../../../widgets/guestBadge'
|
||||
@ -34,6 +35,13 @@ const Entry = (props: EntryComponentProps): ReactElement => {
|
||||
<div className={theme?.mentionSuggestionsEntryText}>
|
||||
{mention.displayName}
|
||||
</div>
|
||||
{!mention.isBoardMember &&
|
||||
<div className={theme?.mentionSuggestionsEntryText}>
|
||||
<FormattedMessage
|
||||
id='MentionSuggestion.is-not-board-member'
|
||||
defaultMessage='(not board member)'
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -18,6 +18,12 @@ import {useAppSelector} from '../../store/hooks'
|
||||
import {IUser} from '../../user'
|
||||
import {getBoardUsersList, getMe} from '../../store/users'
|
||||
import createLiveMarkdownPlugin from '../live-markdown-plugin/liveMarkdownPlugin'
|
||||
import {useHasPermissions} from '../../hooks/permissions'
|
||||
import {Permission} from '../../constants'
|
||||
import {BoardMember} from '../../blocks/board'
|
||||
import mutator from '../../mutator'
|
||||
import ConfirmAddUserForNotifications from '../confirmAddUserForNotifications'
|
||||
import RootPortal from '../rootPortal'
|
||||
|
||||
import './markdownEditorInput.scss'
|
||||
|
||||
@ -34,11 +40,13 @@ import Entry from './entryComponent/entryComponent'
|
||||
const imageURLForUser = (window as any).Components?.imageURLForUser
|
||||
|
||||
type MentionUser = {
|
||||
user: IUser,
|
||||
name: string
|
||||
avatar: string
|
||||
is_bot: boolean
|
||||
is_guest: boolean
|
||||
displayName: string
|
||||
isBoardMember: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
@ -56,6 +64,8 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
||||
const board = useAppSelector(getCurrentBoard)
|
||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||
const ref = useRef<Editor>(null)
|
||||
const allowAddUsers = useHasPermissions(board.teamId, board.id, [Permission.ManageBoardRoles])
|
||||
const [confirmAddUser, setConfirmAddUser] = useState<IUser|null>(null)
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
|
||||
const [suggestions, setSuggestions] = useState<Array<MentionUser>>([])
|
||||
@ -63,7 +73,7 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
||||
const loadSuggestions = async (term: string) => {
|
||||
let users: Array<IUser>
|
||||
|
||||
if (!me?.is_guest && (board && board.type === BoardTypeOpen)) {
|
||||
if (!me?.is_guest && (allowAddUsers || (board && board.type === BoardTypeOpen))) {
|
||||
users = await octoClient.searchTeamUsers(term)
|
||||
} else {
|
||||
users = boardUsers
|
||||
@ -83,8 +93,10 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
||||
avatar: `${imageURLForUser ? imageURLForUser(user.id) : ''}`,
|
||||
is_bot: user.is_bot,
|
||||
is_guest: user.is_guest,
|
||||
displayName: Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}
|
||||
))
|
||||
displayName: Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay),
|
||||
isBoardMember: Boolean(boardUsers.find((u) => u.id === user.id)),
|
||||
user: user,
|
||||
}))
|
||||
setSuggestions(mentions)
|
||||
}
|
||||
|
||||
@ -96,7 +108,6 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
||||
loadSuggestions('')
|
||||
}, [])
|
||||
|
||||
|
||||
const generateEditorState = (text?: string) => {
|
||||
const state = EditorState.createWithContent(ContentState.createFromText(text || ''))
|
||||
return EditorState.moveSelectionToEnd(state)
|
||||
@ -104,6 +115,39 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
||||
|
||||
const [editorState, setEditorState] = useState(() => generateEditorState(initialText))
|
||||
|
||||
const addUser = useCallback(async (userId: string, role: string) => {
|
||||
const newMember = {
|
||||
boardId: board.id,
|
||||
userId: userId,
|
||||
roles: role,
|
||||
schemeAdmin: role === 'Admin',
|
||||
schemeEditor: role === 'Admin' || role === 'Editor',
|
||||
schemeCommenter: role === 'Admin' || role === 'Editor' || role === 'Commenter',
|
||||
schemeViewer: role === 'Admin' || role === 'Editor' || role === 'Commenter' || role === 'Viewer',
|
||||
} as BoardMember
|
||||
|
||||
setConfirmAddUser(null)
|
||||
setEditorState(EditorState.moveSelectionToEnd(editorState))
|
||||
ref.current?.focus()
|
||||
await mutator.createBoardMember(board.id, newMember.userId)
|
||||
mutator.updateBoardMember(newMember, {...newMember, schemeAdmin: false, schemeEditor: true, schemeCommenter: true, schemeViewer: true})
|
||||
}, [board, editorState])
|
||||
|
||||
const [initialTextCache, setInitialTextCache] = useState<string | undefined>(initialText)
|
||||
|
||||
// avoiding stale closure
|
||||
useEffect(() => {
|
||||
// only change editor state when initialText actually changes from one defined value to another.
|
||||
// This is needed to make the mentions plugin work. For some reason, if we don't check
|
||||
// for this if condition here, mentions don't work. I suspect it's because without
|
||||
// the in condition, we're changing editor state twice during component initialization
|
||||
// and for some reason it causes mentions to not show up.
|
||||
if (initialText && initialText !== initialTextCache) {
|
||||
setEditorState(generateEditorState(initialText || ''))
|
||||
setInitialTextCache(initialText)
|
||||
}
|
||||
}, [initialText])
|
||||
|
||||
const [isMentionPopoverOpen, setIsMentionPopoverOpen] = useState(false)
|
||||
const [isEmojiPopoverOpen, setIsEmojiPopoverOpen] = useState(false)
|
||||
|
||||
@ -177,6 +221,9 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
||||
}, [])
|
||||
|
||||
const onEditorStateBlur = useCallback(() => {
|
||||
if (confirmAddUser) {
|
||||
return
|
||||
}
|
||||
const text = editorState.getCurrentContent().getPlainText()
|
||||
onBlur && onBlur(text)
|
||||
}, [editorState, onBlur])
|
||||
@ -220,11 +267,29 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
||||
suggestions={suggestions}
|
||||
onSearchChange={onSearchChange}
|
||||
entryComponent={Entry}
|
||||
onAddMention={(mention) => {
|
||||
if (mention.isBoardMember) {
|
||||
return
|
||||
}
|
||||
setConfirmAddUser(mention.user)
|
||||
}}
|
||||
/>
|
||||
<EmojiSuggestions
|
||||
onOpen={onEmojiPopoverOpen}
|
||||
onClose={onEmojiPopoverClose}
|
||||
/>
|
||||
{confirmAddUser &&
|
||||
<RootPortal>
|
||||
<ConfirmAddUserForNotifications
|
||||
user={confirmAddUser}
|
||||
onConfirm={addUser}
|
||||
onClose={() => {
|
||||
setConfirmAddUser(null)
|
||||
setEditorState(EditorState.moveSelectionToEnd(editorState))
|
||||
ref.current?.focus()
|
||||
}}
|
||||
/>
|
||||
</RootPortal>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import userEvent from '@testing-library/user-event'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
import {IUser} from '../user'
|
||||
import octoClient from '../octoClient'
|
||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||
import {mockDOM, mockMatchMedia, mockStateStore, wrapDNDIntl} from '../testUtils'
|
||||
import {Constants} from '../constants'
|
||||
@ -21,8 +22,10 @@ import Workspace from './workspace'
|
||||
Object.defineProperty(Constants, 'versionString', {value: '1.0.0'})
|
||||
jest.useFakeTimers()
|
||||
jest.mock('../utils')
|
||||
jest.mock('../octoClient')
|
||||
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
|
||||
const mockedUtils = mocked(Utils, true)
|
||||
const mockedOctoClient= mocked(octoClient, true)
|
||||
const board = TestBlockFactory.createBoard()
|
||||
board.id = 'board1'
|
||||
board.teamId = 'team-id'
|
||||
@ -170,6 +173,7 @@ describe('src/components/workspace', () => {
|
||||
],
|
||||
},
|
||||
}
|
||||
mockedOctoClient.searchTeamUsers.mockResolvedValue(Object.values(state.users.boardUsers))
|
||||
const store = mockStateStore([thunk], state)
|
||||
beforeAll(() => {
|
||||
mockDOM()
|
||||
|
@ -85,7 +85,7 @@ describe('properties/createdBy', () => {
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
const {container} = render(wrapIntl(component))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
@ -1,18 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react'
|
||||
import Select from 'react-select'
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import Select from 'react-select/async'
|
||||
import {useIntl} from 'react-intl'
|
||||
import {CSSObject} from '@emotion/serialize'
|
||||
|
||||
import {Utils} from '../../utils'
|
||||
import {IUser} from '../../user'
|
||||
import {getBoardUsersList, getBoardUsers} from '../../store/users'
|
||||
import {BoardMember} from '../../blocks/board'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import mutator from '../../mutator'
|
||||
import {getSelectBaseStyle} from '../../theme'
|
||||
import {ClientConfig} from '../../config/clientConfig'
|
||||
import {getClientConfig} from '../../store/clientConfig'
|
||||
import {useHasPermissions} from '../../hooks/permissions'
|
||||
import {Permission} from '../../constants'
|
||||
import client from '../../octoClient'
|
||||
import ConfirmAddUserForNotifications from '../../components/confirmAddUserForNotifications'
|
||||
import GuestBadge from '../../widgets/guestBadge'
|
||||
|
||||
import {PropertyProps} from '../types'
|
||||
@ -55,6 +61,7 @@ const selectStyles = {
|
||||
|
||||
const Person = (props: PropertyProps): JSX.Element => {
|
||||
const {card, board, propertyTemplate, propertyValue, readOnly} = props
|
||||
const [confirmAddUser, setConfirmAddUser] = useState<IUser|null>(null)
|
||||
|
||||
const boardUsersById = useAppSelector<{[key:string]: IUser}>(getBoardUsers)
|
||||
const onChange = useCallback((newValue) => mutator.changePropertyValue(board.id, card, propertyTemplate.id, newValue), [board.id, card, propertyTemplate.id])
|
||||
@ -62,6 +69,7 @@ const Person = (props: PropertyProps): JSX.Element => {
|
||||
const me: IUser = boardUsersById[propertyValue as string]
|
||||
|
||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||
const intl = useIntl()
|
||||
|
||||
const formatOptionLabel = (user: IUser) => {
|
||||
let profileImg
|
||||
@ -83,6 +91,23 @@ const Person = (props: PropertyProps): JSX.Element => {
|
||||
)
|
||||
}
|
||||
|
||||
const addUser = useCallback(async (userId: string, role: string) => {
|
||||
const newMember = {
|
||||
boardId: board.id,
|
||||
userId: userId,
|
||||
roles: role,
|
||||
schemeAdmin: role === 'Admin',
|
||||
schemeEditor: role === 'Admin' || role === 'Editor',
|
||||
schemeCommenter: role === 'Admin' || role === 'Editor' || role === 'Commenter',
|
||||
schemeViewer: role === 'Admin' || role === 'Editor' || role === 'Commenter' || role === 'Viewer',
|
||||
} as BoardMember
|
||||
|
||||
setConfirmAddUser(null)
|
||||
await mutator.createBoardMember(board.id, newMember.userId)
|
||||
await mutator.changePropertyValue(board.id, card, propertyTemplate.id, newMember.userId)
|
||||
mutator.updateBoardMember(newMember, {...newMember, schemeAdmin: false, schemeEditor: true, schemeCommenter: true, schemeViewer: true})
|
||||
}, [board, card, propertyTemplate])
|
||||
|
||||
if (readOnly) {
|
||||
return (
|
||||
<div className={`Person ${props.property.valueClassName(true)}`}>
|
||||
@ -93,28 +118,66 @@ const Person = (props: PropertyProps): JSX.Element => {
|
||||
|
||||
const boardUsers = useAppSelector<IUser[]>(getBoardUsersList)
|
||||
|
||||
const allowAddUsers = useHasPermissions(board.teamId, board.id, [Permission.ManageBoardRoles])
|
||||
|
||||
const loadOptions = useCallback(async (value: string) => {
|
||||
if (value === '') {
|
||||
return boardUsers
|
||||
}
|
||||
if (!allowAddUsers) {
|
||||
return boardUsers.filter((u) => u.username.toLowerCase().includes(value.toLowerCase()))
|
||||
}
|
||||
const allUsers = await client.searchTeamUsers(value)
|
||||
const usersInsideBoard: IUser[] = []
|
||||
const usersOutsideBoard: IUser[] = []
|
||||
for (const u of allUsers) {
|
||||
if (boardUsersById[u.id]) {
|
||||
usersInsideBoard.push(u)
|
||||
} else {
|
||||
usersOutsideBoard.push(u)
|
||||
}
|
||||
}
|
||||
return [
|
||||
{label: intl.formatMessage({id: 'PersonProperty.board-members', defaultMessage: 'Board members'}), options: usersInsideBoard},
|
||||
{label: intl.formatMessage({id: 'PersonProperty.non-board-members', defaultMessage: 'Not board members'}), options: usersOutsideBoard},
|
||||
]
|
||||
}, [boardUsers, allowAddUsers, boardUsersById])
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={boardUsers}
|
||||
isSearchable={true}
|
||||
isClearable={true}
|
||||
backspaceRemovesValue={true}
|
||||
className={`Person ${props.property.valueClassName(props.readOnly)}`}
|
||||
classNamePrefix={'react-select'}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={selectStyles}
|
||||
placeholder={'Empty'}
|
||||
getOptionLabel={(o: IUser) => o.username}
|
||||
getOptionValue={(a: IUser) => a.id}
|
||||
value={boardUsersById[propertyValue as string] || null}
|
||||
onChange={(item, action) => {
|
||||
if (action.action === 'select-option') {
|
||||
onChange(item?.id || '')
|
||||
} else if (action.action === 'clear') {
|
||||
onChange('')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{confirmAddUser &&
|
||||
<ConfirmAddUserForNotifications
|
||||
user={confirmAddUser}
|
||||
onConfirm={addUser}
|
||||
onClose={() => setConfirmAddUser(null)}
|
||||
/>}
|
||||
<Select
|
||||
loadOptions={loadOptions}
|
||||
defaultOptions={boardUsers}
|
||||
isSearchable={true}
|
||||
isClearable={true}
|
||||
backspaceRemovesValue={true}
|
||||
className={`Person ${props.property.valueClassName(props.readOnly)}`}
|
||||
classNamePrefix={'react-select'}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={selectStyles}
|
||||
placeholder={'Empty'}
|
||||
getOptionLabel={(o: IUser) => o.username}
|
||||
getOptionValue={(a: IUser) => a.id}
|
||||
value={boardUsersById[propertyValue as string] || null}
|
||||
onChange={(item, action) => {
|
||||
if (action.action === 'select-option') {
|
||||
if (!boardUsersById[item?.id || '']) {
|
||||
setConfirmAddUser(item)
|
||||
} else {
|
||||
onChange(item?.id || '')
|
||||
}
|
||||
} else if (action.action === 'clear') {
|
||||
onChange('')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import configureStore from 'redux-mock-store'
|
||||
|
||||
import {createCard} from '../../blocks/card'
|
||||
import {IUser} from '../../user'
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
|
||||
import {createBoard, IPropertyTemplate} from '../../blocks/board'
|
||||
|
||||
@ -64,7 +65,7 @@ describe('properties/updatedBy', () => {
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
const {container} = render(wrapIntl(component))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
$z-index: (
|
||||
// key: value
|
||||
modal-permissions-label: 1000,
|
||||
board-template-selector: 1000,
|
||||
notification-box: 1000,
|
||||
calculation-dropdown: 999,
|
||||
|
Loading…
x
Reference in New Issue
Block a user