mirror of
https://github.com/mattermost/focalboard.git
synced 2025-03-29 21:01:01 +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 {mocked} from 'jest-mock'
|
||||||
|
|
||||||
import mutator from '../mutator'
|
import mutator from '../mutator'
|
||||||
|
import {IUser} from '../user'
|
||||||
import {Utils} from '../utils'
|
import {Utils} from '../utils'
|
||||||
|
import octoClient from '../octoClient'
|
||||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||||
import {mockDOM, mockStateStore, wrapDNDIntl} from '../testUtils'
|
import {mockDOM, mockStateStore, wrapDNDIntl} from '../testUtils'
|
||||||
|
|
||||||
import CardDialog from './cardDialog'
|
import CardDialog from './cardDialog'
|
||||||
|
|
||||||
jest.mock('../mutator')
|
jest.mock('../mutator')
|
||||||
|
jest.mock('../octoClient')
|
||||||
jest.mock('../utils')
|
jest.mock('../utils')
|
||||||
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
|
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
|
||||||
|
|
||||||
const mockedUtils = mocked(Utils, true)
|
const mockedUtils = mocked(Utils, true)
|
||||||
const mockedMutator = mocked(mutator, true)
|
const mockedMutator = mocked(mutator, true)
|
||||||
|
const mockedOctoClient = mocked(octoClient, true)
|
||||||
mockedUtils.createGuid.mockReturnValue('test-id')
|
mockedUtils.createGuid.mockReturnValue('test-id')
|
||||||
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
|
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
|
||||||
|
|
||||||
@ -84,6 +88,8 @@ describe('components/cardDialog', () => {
|
|||||||
blockSubscriptions: [],
|
blockSubscriptions: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mockedOctoClient.searchTeamUsers.mockResolvedValue(Object.values(state.users.boardUsers) as IUser[])
|
||||||
const store = mockStateStore([], state)
|
const store = mockStateStore([], state)
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
@ -10,6 +10,8 @@ import {mockDOM, mockStateStore, wrapDNDIntl} from '../testUtils'
|
|||||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||||
import {IPropertyTemplate} from '../blocks/board'
|
import {IPropertyTemplate} from '../blocks/board'
|
||||||
import {Utils} from '../utils'
|
import {Utils} from '../utils'
|
||||||
|
import {IUser} from '../user'
|
||||||
|
import octoClient from '../octoClient'
|
||||||
import Mutator from '../mutator'
|
import Mutator from '../mutator'
|
||||||
import {Constants} from '../constants'
|
import {Constants} from '../constants'
|
||||||
|
|
||||||
@ -26,11 +28,13 @@ jest.mock('react-router-dom', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
jest.mock('../utils')
|
jest.mock('../utils')
|
||||||
|
jest.mock('../octoClient')
|
||||||
jest.mock('../mutator')
|
jest.mock('../mutator')
|
||||||
jest.mock('../telemetry/telemetryClient')
|
jest.mock('../telemetry/telemetryClient')
|
||||||
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
|
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
|
||||||
const mockedUtils = mocked(Utils, true)
|
const mockedUtils = mocked(Utils, true)
|
||||||
const mockedMutator = mocked(Mutator, true)
|
const mockedMutator = mocked(Mutator, true)
|
||||||
|
const mockedOctoClient= mocked(octoClient, true)
|
||||||
mockedUtils.createGuid.mockReturnValue('test-id')
|
mockedUtils.createGuid.mockReturnValue('test-id')
|
||||||
mockedUtils.generateClassName = jest.requireActual('../utils').Utils.generateClassName
|
mockedUtils.generateClassName = jest.requireActual('../utils').Utils.generateClassName
|
||||||
describe('components/centerPanel', () => {
|
describe('components/centerPanel', () => {
|
||||||
@ -139,6 +143,7 @@ describe('components/centerPanel', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
mockedOctoClient.searchTeamUsers.mockResolvedValue(Object.values(state.users.boardUsers) as IUser[])
|
||||||
const store = mockStateStore([], state)
|
const store = mockStateStore([], state)
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockDOM()
|
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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import React, {ReactElement} from 'react'
|
import React, {ReactElement} from 'react'
|
||||||
|
import {FormattedMessage} from 'react-intl'
|
||||||
import {EntryComponentProps} from '@draft-js-plugins/mention/lib/MentionSuggestions/Entry/Entry'
|
import {EntryComponentProps} from '@draft-js-plugins/mention/lib/MentionSuggestions/Entry/Entry'
|
||||||
|
|
||||||
import GuestBadge from '../../../widgets/guestBadge'
|
import GuestBadge from '../../../widgets/guestBadge'
|
||||||
@ -34,6 +35,13 @@ const Entry = (props: EntryComponentProps): ReactElement => {
|
|||||||
<div className={theme?.mentionSuggestionsEntryText}>
|
<div className={theme?.mentionSuggestionsEntryText}>
|
||||||
{mention.displayName}
|
{mention.displayName}
|
||||||
</div>
|
</div>
|
||||||
|
{!mention.isBoardMember &&
|
||||||
|
<div className={theme?.mentionSuggestionsEntryText}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='MentionSuggestion.is-not-board-member'
|
||||||
|
defaultMessage='(not board member)'
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -18,6 +18,12 @@ import {useAppSelector} from '../../store/hooks'
|
|||||||
import {IUser} from '../../user'
|
import {IUser} from '../../user'
|
||||||
import {getBoardUsersList, getMe} from '../../store/users'
|
import {getBoardUsersList, getMe} from '../../store/users'
|
||||||
import createLiveMarkdownPlugin from '../live-markdown-plugin/liveMarkdownPlugin'
|
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'
|
import './markdownEditorInput.scss'
|
||||||
|
|
||||||
@ -34,11 +40,13 @@ import Entry from './entryComponent/entryComponent'
|
|||||||
const imageURLForUser = (window as any).Components?.imageURLForUser
|
const imageURLForUser = (window as any).Components?.imageURLForUser
|
||||||
|
|
||||||
type MentionUser = {
|
type MentionUser = {
|
||||||
|
user: IUser,
|
||||||
name: string
|
name: string
|
||||||
avatar: string
|
avatar: string
|
||||||
is_bot: boolean
|
is_bot: boolean
|
||||||
is_guest: boolean
|
is_guest: boolean
|
||||||
displayName: string
|
displayName: string
|
||||||
|
isBoardMember: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -56,6 +64,8 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
|||||||
const board = useAppSelector(getCurrentBoard)
|
const board = useAppSelector(getCurrentBoard)
|
||||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||||
const ref = useRef<Editor>(null)
|
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 me = useAppSelector<IUser|null>(getMe)
|
||||||
|
|
||||||
const [suggestions, setSuggestions] = useState<Array<MentionUser>>([])
|
const [suggestions, setSuggestions] = useState<Array<MentionUser>>([])
|
||||||
@ -63,7 +73,7 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
|||||||
const loadSuggestions = async (term: string) => {
|
const loadSuggestions = async (term: string) => {
|
||||||
let users: Array<IUser>
|
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)
|
users = await octoClient.searchTeamUsers(term)
|
||||||
} else {
|
} else {
|
||||||
users = boardUsers
|
users = boardUsers
|
||||||
@ -83,8 +93,10 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
|||||||
avatar: `${imageURLForUser ? imageURLForUser(user.id) : ''}`,
|
avatar: `${imageURLForUser ? imageURLForUser(user.id) : ''}`,
|
||||||
is_bot: user.is_bot,
|
is_bot: user.is_bot,
|
||||||
is_guest: user.is_guest,
|
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)
|
setSuggestions(mentions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +108,6 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
|||||||
loadSuggestions('')
|
loadSuggestions('')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
const generateEditorState = (text?: string) => {
|
const generateEditorState = (text?: string) => {
|
||||||
const state = EditorState.createWithContent(ContentState.createFromText(text || ''))
|
const state = EditorState.createWithContent(ContentState.createFromText(text || ''))
|
||||||
return EditorState.moveSelectionToEnd(state)
|
return EditorState.moveSelectionToEnd(state)
|
||||||
@ -104,6 +115,39 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
|||||||
|
|
||||||
const [editorState, setEditorState] = useState(() => generateEditorState(initialText))
|
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 [isMentionPopoverOpen, setIsMentionPopoverOpen] = useState(false)
|
||||||
const [isEmojiPopoverOpen, setIsEmojiPopoverOpen] = useState(false)
|
const [isEmojiPopoverOpen, setIsEmojiPopoverOpen] = useState(false)
|
||||||
|
|
||||||
@ -177,6 +221,9 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onEditorStateBlur = useCallback(() => {
|
const onEditorStateBlur = useCallback(() => {
|
||||||
|
if (confirmAddUser) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const text = editorState.getCurrentContent().getPlainText()
|
const text = editorState.getCurrentContent().getPlainText()
|
||||||
onBlur && onBlur(text)
|
onBlur && onBlur(text)
|
||||||
}, [editorState, onBlur])
|
}, [editorState, onBlur])
|
||||||
@ -220,11 +267,29 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
|||||||
suggestions={suggestions}
|
suggestions={suggestions}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
entryComponent={Entry}
|
entryComponent={Entry}
|
||||||
|
onAddMention={(mention) => {
|
||||||
|
if (mention.isBoardMember) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setConfirmAddUser(mention.user)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<EmojiSuggestions
|
<EmojiSuggestions
|
||||||
onOpen={onEmojiPopoverOpen}
|
onOpen={onEmojiPopoverOpen}
|
||||||
onClose={onEmojiPopoverClose}
|
onClose={onEmojiPopoverClose}
|
||||||
/>
|
/>
|
||||||
|
{confirmAddUser &&
|
||||||
|
<RootPortal>
|
||||||
|
<ConfirmAddUserForNotifications
|
||||||
|
user={confirmAddUser}
|
||||||
|
onConfirm={addUser}
|
||||||
|
onClose={() => {
|
||||||
|
setConfirmAddUser(null)
|
||||||
|
setEditorState(EditorState.moveSelectionToEnd(editorState))
|
||||||
|
ref.current?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RootPortal>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import userEvent from '@testing-library/user-event'
|
|||||||
import thunk from 'redux-thunk'
|
import thunk from 'redux-thunk'
|
||||||
|
|
||||||
import {IUser} from '../user'
|
import {IUser} from '../user'
|
||||||
|
import octoClient from '../octoClient'
|
||||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||||
import {mockDOM, mockMatchMedia, mockStateStore, wrapDNDIntl} from '../testUtils'
|
import {mockDOM, mockMatchMedia, mockStateStore, wrapDNDIntl} from '../testUtils'
|
||||||
import {Constants} from '../constants'
|
import {Constants} from '../constants'
|
||||||
@ -21,8 +22,10 @@ import Workspace from './workspace'
|
|||||||
Object.defineProperty(Constants, 'versionString', {value: '1.0.0'})
|
Object.defineProperty(Constants, 'versionString', {value: '1.0.0'})
|
||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
jest.mock('../utils')
|
jest.mock('../utils')
|
||||||
|
jest.mock('../octoClient')
|
||||||
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
|
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
|
||||||
const mockedUtils = mocked(Utils, true)
|
const mockedUtils = mocked(Utils, true)
|
||||||
|
const mockedOctoClient= mocked(octoClient, true)
|
||||||
const board = TestBlockFactory.createBoard()
|
const board = TestBlockFactory.createBoard()
|
||||||
board.id = 'board1'
|
board.id = 'board1'
|
||||||
board.teamId = 'team-id'
|
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)
|
const store = mockStateStore([thunk], state)
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockDOM()
|
mockDOM()
|
||||||
|
@ -85,7 +85,7 @@ describe('properties/createdBy', () => {
|
|||||||
</ReduxProvider>
|
</ReduxProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
const {container} = render(component)
|
const {container} = render(wrapIntl(component))
|
||||||
expect(container).toMatchSnapshot()
|
expect(container).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback} from 'react'
|
import React, {useCallback, useState} from 'react'
|
||||||
import Select from 'react-select'
|
import Select from 'react-select/async'
|
||||||
|
import {useIntl} from 'react-intl'
|
||||||
import {CSSObject} from '@emotion/serialize'
|
import {CSSObject} from '@emotion/serialize'
|
||||||
|
|
||||||
import {Utils} from '../../utils'
|
import {Utils} from '../../utils'
|
||||||
import {IUser} from '../../user'
|
import {IUser} from '../../user'
|
||||||
import {getBoardUsersList, getBoardUsers} from '../../store/users'
|
import {getBoardUsersList, getBoardUsers} from '../../store/users'
|
||||||
|
import {BoardMember} from '../../blocks/board'
|
||||||
import {useAppSelector} from '../../store/hooks'
|
import {useAppSelector} from '../../store/hooks'
|
||||||
import mutator from '../../mutator'
|
import mutator from '../../mutator'
|
||||||
import {getSelectBaseStyle} from '../../theme'
|
import {getSelectBaseStyle} from '../../theme'
|
||||||
import {ClientConfig} from '../../config/clientConfig'
|
import {ClientConfig} from '../../config/clientConfig'
|
||||||
import {getClientConfig} from '../../store/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 GuestBadge from '../../widgets/guestBadge'
|
||||||
|
|
||||||
import {PropertyProps} from '../types'
|
import {PropertyProps} from '../types'
|
||||||
@ -55,6 +61,7 @@ const selectStyles = {
|
|||||||
|
|
||||||
const Person = (props: PropertyProps): JSX.Element => {
|
const Person = (props: PropertyProps): JSX.Element => {
|
||||||
const {card, board, propertyTemplate, propertyValue, readOnly} = props
|
const {card, board, propertyTemplate, propertyValue, readOnly} = props
|
||||||
|
const [confirmAddUser, setConfirmAddUser] = useState<IUser|null>(null)
|
||||||
|
|
||||||
const boardUsersById = useAppSelector<{[key:string]: IUser}>(getBoardUsers)
|
const boardUsersById = useAppSelector<{[key:string]: IUser}>(getBoardUsers)
|
||||||
const onChange = useCallback((newValue) => mutator.changePropertyValue(board.id, card, propertyTemplate.id, newValue), [board.id, card, propertyTemplate.id])
|
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 me: IUser = boardUsersById[propertyValue as string]
|
||||||
|
|
||||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
const formatOptionLabel = (user: IUser) => {
|
const formatOptionLabel = (user: IUser) => {
|
||||||
let profileImg
|
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) {
|
if (readOnly) {
|
||||||
return (
|
return (
|
||||||
<div className={`Person ${props.property.valueClassName(true)}`}>
|
<div className={`Person ${props.property.valueClassName(true)}`}>
|
||||||
@ -93,28 +118,66 @@ const Person = (props: PropertyProps): JSX.Element => {
|
|||||||
|
|
||||||
const boardUsers = useAppSelector<IUser[]>(getBoardUsersList)
|
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 (
|
return (
|
||||||
<Select
|
<>
|
||||||
options={boardUsers}
|
{confirmAddUser &&
|
||||||
isSearchable={true}
|
<ConfirmAddUserForNotifications
|
||||||
isClearable={true}
|
user={confirmAddUser}
|
||||||
backspaceRemovesValue={true}
|
onConfirm={addUser}
|
||||||
className={`Person ${props.property.valueClassName(props.readOnly)}`}
|
onClose={() => setConfirmAddUser(null)}
|
||||||
classNamePrefix={'react-select'}
|
/>}
|
||||||
formatOptionLabel={formatOptionLabel}
|
<Select
|
||||||
styles={selectStyles}
|
loadOptions={loadOptions}
|
||||||
placeholder={'Empty'}
|
defaultOptions={boardUsers}
|
||||||
getOptionLabel={(o: IUser) => o.username}
|
isSearchable={true}
|
||||||
getOptionValue={(a: IUser) => a.id}
|
isClearable={true}
|
||||||
value={boardUsersById[propertyValue as string] || null}
|
backspaceRemovesValue={true}
|
||||||
onChange={(item, action) => {
|
className={`Person ${props.property.valueClassName(props.readOnly)}`}
|
||||||
if (action.action === 'select-option') {
|
classNamePrefix={'react-select'}
|
||||||
onChange(item?.id || '')
|
formatOptionLabel={formatOptionLabel}
|
||||||
} else if (action.action === 'clear') {
|
styles={selectStyles}
|
||||||
onChange('')
|
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 {createCard} from '../../blocks/card'
|
||||||
import {IUser} from '../../user'
|
import {IUser} from '../../user'
|
||||||
|
import {wrapIntl} from '../../testUtils'
|
||||||
|
|
||||||
import {createBoard, IPropertyTemplate} from '../../blocks/board'
|
import {createBoard, IPropertyTemplate} from '../../blocks/board'
|
||||||
|
|
||||||
@ -64,7 +65,7 @@ describe('properties/updatedBy', () => {
|
|||||||
</ReduxProvider>
|
</ReduxProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
const {container} = render(component)
|
const {container} = render(wrapIntl(component))
|
||||||
expect(container).toMatchSnapshot()
|
expect(container).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
$z-index: (
|
$z-index: (
|
||||||
// key: value
|
// key: value
|
||||||
|
modal-permissions-label: 1000,
|
||||||
board-template-selector: 1000,
|
board-template-selector: 1000,
|
||||||
notification-box: 1000,
|
notification-box: 1000,
|
||||||
calculation-dropdown: 999,
|
calculation-dropdown: 999,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user