mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-26 18:48:15 +02:00
Fixed comments not displaying in readonly view (#820)
* Fixed comments not displaying in readonly view * FIxed some tests * Updated tests Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
This commit is contained in:
parent
beebf70cbc
commit
578b91fd2e
145
webapp/src/components/cardDetail/cardDetail.test.tsx
Normal file
145
webapp/src/components/cardDetail/cardDetail.test.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import 'isomorphic-fetch'
|
||||
import {act, render} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import {mockDOM, wrapIntl} from '../../testUtils'
|
||||
|
||||
import CardDetail from './cardDetail'
|
||||
|
||||
global.fetch = FetchMock.fn
|
||||
|
||||
beforeEach(() => {
|
||||
FetchMock.fn.mockReset()
|
||||
})
|
||||
|
||||
// This is needed to run EasyMDE in tests.
|
||||
// It needs bounding rectangle box property
|
||||
// on HTML elements, but Jest's HTML engine jsdom
|
||||
// doesn't provide it.
|
||||
// So we mock it.
|
||||
beforeAll(() => {
|
||||
mockDOM()
|
||||
})
|
||||
|
||||
describe('components/cardDetail/CardDetail', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
view.fields.sortOptions = []
|
||||
view.fields.groupById = undefined
|
||||
view.fields.hiddenOptionIds = []
|
||||
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
|
||||
const createdAt = Date.parse('01 Jan 2021 00:00:00 GMT')
|
||||
const comment1 = TestBlockFactory.createComment(card)
|
||||
comment1.type = 'comment'
|
||||
comment1.title = 'Comment 1'
|
||||
comment1.parentId = card.id
|
||||
comment1.createAt = createdAt
|
||||
|
||||
const comment2 = TestBlockFactory.createComment(card)
|
||||
comment2.type = 'comment'
|
||||
comment2.title = 'Comment 2'
|
||||
comment2.parentId = card.id
|
||||
comment2.createAt = createdAt
|
||||
|
||||
test('should show comments', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
users: {
|
||||
workspaceUsers: [
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<CardDetail
|
||||
board={board}
|
||||
activeView={view}
|
||||
views={[view]}
|
||||
cards={[card]}
|
||||
card={card}
|
||||
comments={[comment1, comment2]}
|
||||
contents={[]}
|
||||
readonly={false}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toBeDefined()
|
||||
|
||||
// Comments show up
|
||||
const comments = container!.querySelectorAll('.comment-text')
|
||||
expect(comments.length).toBe(2)
|
||||
|
||||
// Add comment option visible when readonly mode is off
|
||||
const newCommentSection = container!.querySelectorAll('.newcomment')
|
||||
expect(newCommentSection.length).toBe(1)
|
||||
})
|
||||
|
||||
test('should show comments in readonly view', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
users: {
|
||||
workspaceUsers: [
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<CardDetail
|
||||
board={board}
|
||||
activeView={view}
|
||||
views={[view]}
|
||||
cards={[card]}
|
||||
card={card}
|
||||
comments={[comment1, comment2]}
|
||||
contents={[]}
|
||||
readonly={true}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toBeDefined()
|
||||
|
||||
// comments show up
|
||||
const comments = container!.querySelectorAll('.comment-text')
|
||||
expect(comments.length).toBe(2)
|
||||
|
||||
// Add comment option is not shown in readonly mode
|
||||
const newCommentSection = container!.querySelectorAll('.newcomment')
|
||||
expect(newCommentSection.length).toBe(0)
|
||||
})
|
||||
})
|
@ -124,17 +124,13 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
||||
|
||||
{/* Comments */}
|
||||
|
||||
{!props.readonly &&
|
||||
<>
|
||||
<hr/>
|
||||
<CommentsList
|
||||
comments={comments}
|
||||
rootId={card.rootId}
|
||||
cardId={card.id}
|
||||
/>
|
||||
<hr/>
|
||||
</>
|
||||
}
|
||||
<hr/>
|
||||
<CommentsList
|
||||
comments={comments}
|
||||
rootId={card.rootId}
|
||||
cardId={card.id}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content blocks */}
|
||||
|
@ -20,6 +20,7 @@ type Props = {
|
||||
comment: Block
|
||||
userId: string
|
||||
userImageUrl: string
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
const Comment: FC<Props> = (props: Props) => {
|
||||
@ -42,17 +43,20 @@ const Comment: FC<Props> = (props: Props) => {
|
||||
<div className='comment-date'>
|
||||
{Utils.displayDateTime(new Date(comment.createAt), intl)}
|
||||
</div>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu position='left'>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'Comment.delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => mutator.deleteBlock(comment)}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
||||
{!props.readonly && (
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu position='left'>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'Comment.delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => mutator.deleteBlock(comment)}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className='comment-text'
|
||||
|
126
webapp/src/components/cardDetail/commentsList.test.tsx
Normal file
126
webapp/src/components/cardDetail/commentsList.test.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import 'isomorphic-fetch'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import {act} from 'react-dom/test-utils'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {CommentBlock} from '../../blocks/commentBlock'
|
||||
|
||||
import {mockDOM, wrapIntl} from '../../testUtils'
|
||||
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
|
||||
import CommentsList from './commentsList'
|
||||
|
||||
global.fetch = FetchMock.fn
|
||||
|
||||
beforeEach(() => {
|
||||
FetchMock.fn.mockReset()
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
mockDOM()
|
||||
})
|
||||
|
||||
describe('components/cardDetail/CommentsList', () => {
|
||||
const createdAt = Date.parse('01 Jan 2021 00:00:00 GMT')
|
||||
const comment1: CommentBlock = {
|
||||
id: 'comment_id_1',
|
||||
title: 'Comment 1',
|
||||
createAt: createdAt,
|
||||
modifiedBy: 'user_id_1',
|
||||
} as CommentBlock
|
||||
|
||||
const comment2: CommentBlock = {
|
||||
id: 'comment_id_2',
|
||||
title: 'Comment 2',
|
||||
createAt: createdAt,
|
||||
modifiedBy: 'user_id_2',
|
||||
} as CommentBlock
|
||||
|
||||
test('comments show up', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
users: {
|
||||
workspaceUsers: [
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<CommentsList
|
||||
comments={[comment1, comment2]}
|
||||
rootId={'root_id'}
|
||||
cardId={'card_id'}
|
||||
readonly={false}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toBeDefined()
|
||||
|
||||
// Comments show up
|
||||
const comments = container!.querySelectorAll('.comment-text')
|
||||
expect(comments.length).toBe(2)
|
||||
|
||||
// Add comment option visible when readonly mode is off
|
||||
const newCommentSection = container!.querySelectorAll('.newcomment')
|
||||
expect(newCommentSection.length).toBe(1)
|
||||
})
|
||||
|
||||
test('comments show up in readonly mode', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
users: {
|
||||
workspaceUsers: [
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<CommentsList
|
||||
comments={[comment1, comment2]}
|
||||
rootId={'root_id'}
|
||||
cardId={'card_id'}
|
||||
readonly={true}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toBeDefined()
|
||||
|
||||
// Comments show up
|
||||
const comments = container!.querySelectorAll('.comment-text')
|
||||
expect(comments.length).toBe(2)
|
||||
|
||||
// Add comment option visible when readonly mode is off
|
||||
const newCommentSection = container!.querySelectorAll('.newcomment')
|
||||
expect(newCommentSection.length).toBe(0)
|
||||
})
|
||||
})
|
@ -17,6 +17,7 @@ type Props = {
|
||||
comments: readonly CommentBlock[]
|
||||
rootId: string
|
||||
cardId: string
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
const CommentsList = React.memo((props: Props) => {
|
||||
@ -44,6 +45,38 @@ const CommentsList = React.memo((props: Props) => {
|
||||
// TODO: Replace this placeholder
|
||||
const userImageUrl = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="fill: rgb(192, 192, 192);"><rect width="100" height="100" /></svg>'
|
||||
|
||||
const newCommentComponent = (
|
||||
<div className='commentrow'>
|
||||
<img
|
||||
className='comment-avatar'
|
||||
src={userImageUrl}
|
||||
/>
|
||||
<MarkdownEditor
|
||||
className='newcomment'
|
||||
text={newComment}
|
||||
placeholderText={intl.formatMessage({id: 'CardDetail.new-comment-placeholder', defaultMessage: 'Add a comment...'})}
|
||||
onChange={(value: string) => {
|
||||
if (newComment !== value) {
|
||||
setNewComment(value)
|
||||
}
|
||||
}}
|
||||
onAccept={onSendClicked}
|
||||
/>
|
||||
|
||||
{newComment &&
|
||||
<Button
|
||||
filled={true}
|
||||
onClick={onSendClicked}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='CommentsList.send'
|
||||
defaultMessage='Send'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='CommentsList'>
|
||||
{comments.map((comment) => (
|
||||
@ -52,40 +85,15 @@ const CommentsList = React.memo((props: Props) => {
|
||||
comment={comment}
|
||||
userImageUrl={userImageUrl}
|
||||
userId={comment.modifiedBy}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* New comment */}
|
||||
{!props.readonly && newCommentComponent}
|
||||
|
||||
<div className='commentrow'>
|
||||
<img
|
||||
className='comment-avatar'
|
||||
src={userImageUrl}
|
||||
/>
|
||||
<MarkdownEditor
|
||||
className='newcomment'
|
||||
text={newComment}
|
||||
placeholderText={intl.formatMessage({id: 'CardDetail.new-comment-placeholder', defaultMessage: 'Add a comment...'})}
|
||||
onChange={(value: string) => {
|
||||
if (newComment !== value) {
|
||||
setNewComment(value)
|
||||
}
|
||||
}}
|
||||
onAccept={onSendClicked}
|
||||
/>
|
||||
|
||||
{newComment &&
|
||||
<Button
|
||||
filled={true}
|
||||
onClick={onSendClicked}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='CommentsList.send'
|
||||
defaultMessage='Send'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
{/* horizontal divider below comments */}
|
||||
{!(comments.length === 0 && props.readonly) && <hr/>}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
25
webapp/src/testUtils.tsx
Normal file
25
webapp/src/testUtils.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IntlProvider} from 'react-intl'
|
||||
import React from 'react'
|
||||
|
||||
export const wrapIntl = (children: any) => <IntlProvider locale='en'>{children}</IntlProvider>
|
||||
|
||||
export function mockDOM(): void {
|
||||
window.focus = jest.fn()
|
||||
document.createRange = () => {
|
||||
const range = new Range()
|
||||
|
||||
range.getBoundingClientRect = jest.fn()
|
||||
|
||||
range.getClientRects = () => {
|
||||
return {
|
||||
item: () => null,
|
||||
length: 0,
|
||||
[Symbol.iterator]: jest.fn(),
|
||||
}
|
||||
}
|
||||
|
||||
return range
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user