mirror of
https://github.com/mattermost/focalboard.git
synced 2025-02-01 19:14:35 +02:00
[GH-1754] Feature: card badges (#2025)
* First shot implementation of badges for the card. * Showing and hiding card badges in board/gallery views via header menu action added. * Counting of checkboxes in markdown supported. * Use Intl.formatMessage for badge titles. * Unit tests for `CardBadges` component added. Some other unit tests fixed. * Unit test for 'Show card badges' action in the view header menu added. * Cypress test for card badges added: - card with comments, description and checkboxes added for testing - card badges are shown and hidden via view menu - new Cypress command `uiAddNewCard` added - label property added to `MenuWrapper` and used in `ViewHeaderActionsMenu` * Unit tests fixed after change of the label for view menu. * Fix stylelint issues. * Class name for `CardBadges` component fixed. * Show and hide for card badges moved to `Properties` menu: - field `cardBadgesVisible` removed from `BoardViewFields` - new constant `badgesColumnId` introduced and used as an element in `visiblePropertyIds` - card badges added to calendar view - added `role` and `aria-label` for menu component `SwitchOption` - unit and Cypress tests updated * Fix Cypress test: use `blur` after typing text. Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
parent
b33b998f9e
commit
342c8df39d
@ -5,7 +5,8 @@
|
||||
"**/login*.ts",
|
||||
"**/create*.ts",
|
||||
"**/manage*.ts",
|
||||
"**/group*.ts"
|
||||
"**/group*.ts",
|
||||
"**/card*.ts"
|
||||
],
|
||||
"env": {
|
||||
"username": "test-user",
|
||||
|
1
webapp/cypress/global.d.ts
vendored
1
webapp/cypress/global.d.ts
vendored
@ -19,6 +19,7 @@ declare namespace Cypress {
|
||||
apiResetBoards: () => Chainable
|
||||
uiCreateNewBoard: (title?: string) => Chainable
|
||||
uiAddNewGroup: (name?: string) => Chainable
|
||||
uiAddNewCard: (title?: string, columnIndex?: number) => Chainable
|
||||
|
||||
/**
|
||||
* Create a board on a given menu item.
|
||||
|
67
webapp/cypress/integration/cardBadges.ts
Normal file
67
webapp/cypress/integration/cardBadges.ts
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
describe('Card badges', () => {
|
||||
beforeEach(() => {
|
||||
cy.apiInitServer()
|
||||
cy.apiResetBoards()
|
||||
localStorage.setItem('welcomePageViewed', 'true')
|
||||
})
|
||||
|
||||
it('Shows and hides card badges', () => {
|
||||
cy.visit('/')
|
||||
|
||||
// Create new board
|
||||
cy.uiCreateNewBoard('Testing')
|
||||
|
||||
// Add a new card
|
||||
cy.uiAddNewCard('Card')
|
||||
|
||||
// Add some comments
|
||||
cy.log('**Add some comments**')
|
||||
addComment('Some comment')
|
||||
addComment('Another comment')
|
||||
addComment('Additional comment')
|
||||
|
||||
// Add card description
|
||||
cy.log('**Add card description**')
|
||||
cy.findByText('Add a description...').click()
|
||||
cy.findByRole('combobox').type('## Header\n- [ ] one\n- [x] two{esc}')
|
||||
|
||||
// Add checkboxes
|
||||
cy.log('**Add checkboxes**')
|
||||
cy.findByRole('button', {name: 'Add content'}).click()
|
||||
cy.findByRole('button', {name: 'checkbox'}).click()
|
||||
cy.findByDisplayValue('').type('three{enter}')
|
||||
cy.findByDisplayValue('').type('four{enter}')
|
||||
cy.findByDisplayValue('').type('{esc}')
|
||||
cy.findByDisplayValue('three').prev().click()
|
||||
|
||||
// Close card dialog
|
||||
cy.log('**Close card dialog**')
|
||||
cy.findByRole('button', {name: 'Close dialog'}).click()
|
||||
cy.findByRole('dialog').should('not.exist')
|
||||
|
||||
// Show card badges
|
||||
cy.log('**Show card badges**')
|
||||
cy.findByRole('button', {name: 'Properties menu'}).click()
|
||||
cy.findByRole('button', {name: 'Comments and Description'}).click()
|
||||
cy.findByTitle('This card has a description').should('exist')
|
||||
cy.findByTitle('Comments').contains('3').should('exist')
|
||||
cy.findByTitle('Checkboxes').contains('2/4').should('exist')
|
||||
|
||||
// Hide card badges
|
||||
cy.log('**Hide card badges**')
|
||||
cy.findByRole('button', {name: 'Properties menu'}).click()
|
||||
cy.findByRole('button', {name: 'Comments and Description'}).click()
|
||||
cy.findByTitle('This card has a description').should('not.exist')
|
||||
cy.findByTitle('Comments').should('not.exist')
|
||||
cy.findByTitle('Checkboxes').should('not.exist')
|
||||
})
|
||||
|
||||
const addComment = (text: string) => {
|
||||
cy.findByText('Add a comment...').click()
|
||||
cy.findByRole('combobox').type(text).blur()
|
||||
cy.findByRole('button', {name: 'Send'}).click()
|
||||
}
|
||||
})
|
@ -126,3 +126,14 @@ Cypress.Commands.add('uiAddNewGroup', (name?: string) => {
|
||||
}
|
||||
cy.wait(500)
|
||||
})
|
||||
|
||||
Cypress.Commands.add('uiAddNewCard', (title?: string, columnIndex?: number) => {
|
||||
cy.log('**Add a new card**')
|
||||
cy.findByRole('button', {name: '+ New'}).eq(columnIndex || 0).click()
|
||||
cy.findByRole('dialog').should('exist')
|
||||
|
||||
if (title) {
|
||||
cy.log('**Change card title**')
|
||||
cy.findByPlaceholderText('Untitled').type(title)
|
||||
}
|
||||
})
|
||||
|
47
webapp/src/components/__snapshots__/cardBadges.test.tsx.snap
Normal file
47
webapp/src/components/__snapshots__/cardBadges.test.tsx.snap
Normal file
@ -0,0 +1,47 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/cardBadges should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="CardBadges "
|
||||
>
|
||||
<span
|
||||
title="This card has a description"
|
||||
>
|
||||
<svg
|
||||
class="TextIcon Icon"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M432 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
title="Comments"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-message-text-outline MessageIcon"
|
||||
/>
|
||||
3
|
||||
</span>
|
||||
<span
|
||||
title="Checkboxes"
|
||||
>
|
||||
<svg
|
||||
class="CheckIcon Icon"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline
|
||||
points="20,60 40,80 80,40"
|
||||
/>
|
||||
</svg>
|
||||
3/7
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/cardBadges should match snapshot for empty card 1`] = `<div />`;
|
@ -168,7 +168,7 @@ exports[`components/centerPanel return centerPanel and click on card to show car
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -233,7 +233,7 @@ exports[`components/centerPanel return centerPanel and click on card to show car
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -727,7 +727,7 @@ exports[`components/centerPanel return centerPanel and click on new card to edit
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -792,7 +792,7 @@ exports[`components/centerPanel return centerPanel and click on new card to edit
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -1789,7 +1789,7 @@ exports[`components/centerPanel return centerPanel and press touch ctrl+d for on
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -1854,7 +1854,7 @@ exports[`components/centerPanel return centerPanel and press touch ctrl+d for on
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -2408,7 +2408,7 @@ exports[`components/centerPanel return centerPanel and press touch del for one c
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -2473,7 +2473,7 @@ exports[`components/centerPanel return centerPanel and press touch del for one c
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -3027,7 +3027,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -3092,7 +3092,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -3646,7 +3646,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -3711,7 +3711,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -4265,7 +4265,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -4330,7 +4330,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -4884,7 +4884,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -4949,7 +4949,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -5503,7 +5503,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -5568,7 +5568,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -6122,7 +6122,7 @@ exports[`components/centerPanel return centerPanel and select one card and click
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -6187,7 +6187,7 @@ exports[`components/centerPanel return centerPanel and select one card and click
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -6741,7 +6741,7 @@ exports[`components/centerPanel return centerPanel and select one card and click
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -6806,7 +6806,7 @@ exports[`components/centerPanel return centerPanel and select one card and click
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -7360,7 +7360,7 @@ exports[`components/centerPanel should match snapshot for Gallery 1`] = `
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -7407,7 +7407,7 @@ exports[`components/centerPanel should match snapshot for Gallery 1`] = `
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -7649,7 +7649,7 @@ exports[`components/centerPanel should match snapshot for Kanban 1`] = `
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -7714,7 +7714,7 @@ exports[`components/centerPanel should match snapshot for Kanban 1`] = `
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -8174,7 +8174,7 @@ exports[`components/centerPanel should match snapshot for Table 1`] = `
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -8239,7 +8239,7 @@ exports[`components/centerPanel should match snapshot for Table 1`] = `
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
|
@ -400,7 +400,7 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -465,7 +465,7 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -1559,7 +1559,7 @@ exports[`src/components/workspace should match snapshot 1`] = `
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -1624,7 +1624,7 @@ exports[`src/components/workspace should match snapshot 1`] = `
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
|
@ -17,6 +17,8 @@ import {Card} from '../../blocks/card'
|
||||
import {DateProperty, createDatePropertyFromString} from '../properties/dateRange/dateRange'
|
||||
import Tooltip from '../../widgets/tooltip'
|
||||
import PropertyValueElement from '../propertyValueElement'
|
||||
import {Constants} from '../../constants'
|
||||
import CardBadges from '../cardBadges'
|
||||
|
||||
import './fullcalendar.scss'
|
||||
|
||||
@ -113,6 +115,8 @@ const CalendarFullView = (props: Props): JSX.Element|null => {
|
||||
})
|
||||
), [cards, dateDisplayProperty])
|
||||
|
||||
const visibleBadges = activeView.fields.visiblePropertyIds.includes(Constants.badgesColumnId)
|
||||
|
||||
const renderEventContent = (eventProps: EventContentArg): JSX.Element|null => {
|
||||
const {event} = eventProps
|
||||
return (
|
||||
@ -140,6 +144,8 @@ const CalendarFullView = (props: Props): JSX.Element|null => {
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
{visibleBadges &&
|
||||
<CardBadges card={cards.find((o) => o.id === event.id) || cards[0]}/> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
13
webapp/src/components/cardBadges.scss
Normal file
13
webapp/src/components/cardBadges.scss
Normal file
@ -0,0 +1,13 @@
|
||||
.CardBadges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin-right: 8px;
|
||||
|
||||
.Icon,
|
||||
.CompassIcon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
82
webapp/src/components/cardBadges.test.tsx
Normal file
82
webapp/src/components/cardBadges.test.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {render, screen} from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||
import {blocksById, mockStateStore, wrapDNDIntl} from '../testUtils'
|
||||
|
||||
import {RootState} from '../store'
|
||||
|
||||
import {CommentBlock} from '../blocks/commentBlock'
|
||||
|
||||
import {CheckboxBlock} from '../blocks/checkboxBlock'
|
||||
|
||||
import CardBadges from './cardBadges'
|
||||
|
||||
describe('components/cardBadges', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const emptyCard = TestBlockFactory.createCard(board)
|
||||
const text = TestBlockFactory.createText(card)
|
||||
text.title = `
|
||||
## Header
|
||||
- [x] one
|
||||
- [ ] two
|
||||
- [x] three
|
||||
`.replace(/\n\s+/gm, '\n')
|
||||
const comments = Array.from(Array<CommentBlock>(3), () => TestBlockFactory.createComment(card))
|
||||
const checkboxes = Array.from(Array<CheckboxBlock>(4), () => TestBlockFactory.createCheckbox(card))
|
||||
checkboxes[2].fields.value = true
|
||||
|
||||
const state: Partial<RootState> = {
|
||||
cards: {
|
||||
current: '',
|
||||
cards: blocksById([card, emptyCard]),
|
||||
templates: {},
|
||||
},
|
||||
comments: {
|
||||
comments: blocksById(comments),
|
||||
},
|
||||
contents: {
|
||||
contents: {
|
||||
...blocksById([text]),
|
||||
...blocksById(checkboxes),
|
||||
},
|
||||
},
|
||||
}
|
||||
const store = mockStateStore([], state)
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardBadges card={card}/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should match snapshot for empty card', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardBadges card={emptyCard}/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render correct values', () => {
|
||||
render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardBadges card={card}/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
expect(screen.getByTitle(/card has a description/)).toBeInTheDocument()
|
||||
expect(screen.getByTitle('Comments')).toHaveTextContent('3')
|
||||
expect(screen.getByTitle('Checkboxes')).toHaveTextContent('3/7')
|
||||
})
|
||||
})
|
107
webapp/src/components/cardBadges.tsx
Normal file
107
webapp/src/components/cardBadges.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useMemo} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {Card} from '../blocks/card'
|
||||
import {useAppSelector} from '../store/hooks'
|
||||
import {getCardContents} from '../store/contents'
|
||||
import {getCardComments} from '../store/comments'
|
||||
import {ContentBlock} from '../blocks/contentBlock'
|
||||
import {CommentBlock} from '../blocks/commentBlock'
|
||||
import TextIcon from '../widgets/icons/text'
|
||||
import MessageIcon from '../widgets/icons/message'
|
||||
import CheckIcon from '../widgets/icons/check'
|
||||
import {Utils} from '../utils'
|
||||
|
||||
import './cardBadges.scss'
|
||||
|
||||
type Props = {
|
||||
card: Card
|
||||
className?: string
|
||||
}
|
||||
|
||||
type Checkboxes = {
|
||||
total: number
|
||||
checked: number
|
||||
}
|
||||
|
||||
type Badges = {
|
||||
description: boolean
|
||||
comments: number
|
||||
checkboxes: Checkboxes
|
||||
}
|
||||
|
||||
const hasBadges = (badges: Badges): boolean => {
|
||||
return badges.description || badges.comments > 0 || badges.checkboxes.total > 0
|
||||
}
|
||||
|
||||
type ContentsType = Array<ContentBlock | ContentBlock[]>
|
||||
|
||||
const calculateBadges = (contents: ContentsType, comments: CommentBlock[]): Badges => {
|
||||
let text = 0
|
||||
let total = 0
|
||||
let checked = 0
|
||||
|
||||
const updateCounters = (block: ContentBlock) => {
|
||||
if (block.type === 'text') {
|
||||
text++
|
||||
const checkboxes = Utils.countCheckboxesInMarkdown(block.title)
|
||||
total += checkboxes.total
|
||||
checked += checkboxes.checked
|
||||
} else if (block.type === 'checkbox') {
|
||||
total++
|
||||
if (block.fields.value) {
|
||||
checked++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const content of contents) {
|
||||
if (Array.isArray(content)) {
|
||||
content.forEach(updateCounters)
|
||||
} else {
|
||||
updateCounters(content)
|
||||
}
|
||||
}
|
||||
return {
|
||||
description: text > 0,
|
||||
comments: comments.length,
|
||||
checkboxes: {
|
||||
total,
|
||||
checked,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const CardBadges = (props: Props) => {
|
||||
const {card, className} = props
|
||||
const contents = useAppSelector(getCardContents(card.id))
|
||||
const comments = useAppSelector(getCardComments(card.id))
|
||||
const badges = useMemo(() => calculateBadges(contents, comments), [contents, comments])
|
||||
if (!hasBadges(badges)) {
|
||||
return null
|
||||
}
|
||||
const intl = useIntl()
|
||||
const {checkboxes} = badges
|
||||
return (
|
||||
<div className={`CardBadges ${className || ''}`}>
|
||||
{badges.description &&
|
||||
<span title={intl.formatMessage({id: 'CardBadges.title-description', defaultMessage: 'This card has a description'})}>
|
||||
<TextIcon/>
|
||||
</span>}
|
||||
{badges.comments > 0 &&
|
||||
<span title={intl.formatMessage({id: 'CardBadges.title-comments', defaultMessage: 'Comments'})}>
|
||||
<MessageIcon/>
|
||||
{badges.comments}
|
||||
</span>}
|
||||
{checkboxes.total > 0 &&
|
||||
<span title={intl.formatMessage({id: 'CardBadges.title-checkboxes', defaultMessage: 'Checkboxes'})}>
|
||||
<CheckIcon/>
|
||||
{`${checkboxes.checked}/${checkboxes.total}`}
|
||||
</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(CardBadges)
|
@ -26,7 +26,14 @@ exports[`src/components/gallery/Gallery return Gallery and click new 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
class="DividerElement"
|
||||
/>
|
||||
<div
|
||||
class="DividerElement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="GalleryCard"
|
||||
@ -72,7 +79,14 @@ exports[`src/components/gallery/Gallery return Gallery readonly 1`] = `
|
||||
>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
class="DividerElement"
|
||||
/>
|
||||
<div
|
||||
class="DividerElement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="GalleryCard"
|
||||
@ -200,7 +214,14 @@ exports[`src/components/gallery/Gallery should match snapshot 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
class="DividerElement"
|
||||
/>
|
||||
<div
|
||||
class="DividerElement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="GalleryCard"
|
||||
|
@ -10,12 +10,14 @@ import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
|
||||
import {wrapDNDIntl, mockStateStore} from '../../testUtils'
|
||||
import {wrapDNDIntl, mockStateStore, blocksById} from '../../testUtils'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import mutator from '../../mutator'
|
||||
|
||||
import {RootState} from '../../store'
|
||||
|
||||
import Gallery from './gallery'
|
||||
|
||||
jest.mock('../../mutator')
|
||||
@ -28,12 +30,16 @@ describe('src/components/gallery/Gallery', () => {
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const card2 = TestBlockFactory.createCard(board)
|
||||
const contents = [TestBlockFactory.createDivider(card), TestBlockFactory.createDivider(card), TestBlockFactory.createDivider(card2)]
|
||||
const state = {
|
||||
contents,
|
||||
const state: Partial<RootState> = {
|
||||
contents: {
|
||||
contents: blocksById(contents),
|
||||
},
|
||||
cards: {
|
||||
current: '',
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
templates: {},
|
||||
},
|
||||
comments: {
|
||||
comments: {},
|
||||
|
@ -53,6 +53,7 @@ const Gallery = (props: Props): JSX.Element => {
|
||||
}
|
||||
|
||||
const visibleTitle = activeView.fields.visiblePropertyIds.includes(Constants.titleColumnId)
|
||||
const visibleBadges = activeView.fields.visiblePropertyIds.includes(Constants.badgesColumnId)
|
||||
|
||||
return (
|
||||
<div className='Gallery'>
|
||||
@ -65,6 +66,7 @@ const Gallery = (props: Props): JSX.Element => {
|
||||
onClick={props.onCardClicked}
|
||||
visiblePropertyTemplates={visiblePropertyTemplates}
|
||||
visibleTitle={visibleTitle}
|
||||
visibleBadges={visibleBadges}
|
||||
isSelected={props.selectedCardIds.includes(card.id)}
|
||||
readonly={props.readonly}
|
||||
onDrop={onDropToCard}
|
||||
|
@ -76,6 +76,7 @@
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
overflow-wrap: anywhere;
|
||||
font-weight: 600;
|
||||
|
||||
.octo-icon {
|
||||
margin-right: 5px;
|
||||
@ -95,4 +96,8 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-badges {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[{id: card.id, name: 'testTemplateProperty', type: 'text', options: [{id: '1', value: 'testValue', color: 'blue'}]}]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -105,6 +106,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -125,6 +127,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -150,6 +153,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -174,6 +178,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -197,6 +202,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -240,6 +246,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -288,6 +295,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -331,6 +339,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -351,6 +360,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={true}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -392,6 +402,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -412,6 +423,7 @@ describe('src/components/gallery/GalleryCard', () => {
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
visibleBadges={false}
|
||||
readonly={true}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
|
@ -27,6 +27,7 @@ import ImageElement from '../content/imageElement'
|
||||
import {sendFlashMessage} from '../flashMessages'
|
||||
import PropertyValueElement from '../propertyValueElement'
|
||||
import './galleryCard.scss'
|
||||
import CardBadges from '../cardBadges'
|
||||
|
||||
type Props = {
|
||||
board: Board
|
||||
@ -35,6 +36,7 @@ type Props = {
|
||||
visiblePropertyTemplates: IPropertyTemplate[]
|
||||
visibleTitle: boolean
|
||||
isSelected: boolean
|
||||
visibleBadges: boolean
|
||||
readonly: boolean
|
||||
isManualSort: boolean
|
||||
onDrop: (srcCard: Card, dstCard: Card) => void
|
||||
@ -176,6 +178,11 @@ const GalleryCard = React.memo((props: Props) => {
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>}
|
||||
{props.visibleBadges &&
|
||||
<CardBadges
|
||||
card={card}
|
||||
className='gallery-badges'
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
@ -414,7 +414,9 @@ exports[`components/sidebar/GlobalHeaderSettingsMenu languages menu open should
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Random icons"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -569,7 +571,9 @@ exports[`components/sidebar/GlobalHeaderSettingsMenu settings menu open should m
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Random icons"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
|
@ -14,6 +14,7 @@ import {BoardView} from '../../blocks/boardView'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils, IDType} from '../../utils'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import {Constants} from '../../constants'
|
||||
|
||||
import {dragAndDropRearrange} from '../cardDetail/cardDetailContentsUtility'
|
||||
|
||||
@ -53,6 +54,7 @@ const Kanban = (props: Props) => {
|
||||
const visiblePropertyTemplates =
|
||||
activeView.fields.visiblePropertyIds.map((id) => board.fields.cardProperties.find((t) => t.id === id)).filter((i) => i) as IPropertyTemplate[]
|
||||
const isManualSort = activeView.fields.sortOptions.length === 0
|
||||
const visibleBadges = activeView.fields.visiblePropertyIds.includes(Constants.badgesColumnId)
|
||||
|
||||
const propertyNameChanged = useCallback(async (option: IPropertyOption, text: string): Promise<void> => {
|
||||
await mutator.changePropertyOptionValue(board, groupByProperty!, option, text)
|
||||
@ -266,6 +268,7 @@ const Kanban = (props: Props) => {
|
||||
card={card}
|
||||
board={board}
|
||||
visiblePropertyTemplates={visiblePropertyTemplates}
|
||||
visibleBadges={visibleBadges}
|
||||
key={card.id}
|
||||
readonly={props.readonly}
|
||||
isSelected={props.selectedCardIds.includes(card.id)}
|
||||
|
@ -63,6 +63,7 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
card={card}
|
||||
board={board}
|
||||
visiblePropertyTemplates={[propertyTemplate]}
|
||||
visibleBadges={false}
|
||||
isSelected={false}
|
||||
readonly={false}
|
||||
onDrop={jest.fn()}
|
||||
@ -80,6 +81,7 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
card={card}
|
||||
board={board}
|
||||
visiblePropertyTemplates={[propertyTemplate]}
|
||||
visibleBadges={false}
|
||||
isSelected={false}
|
||||
readonly={true}
|
||||
onDrop={jest.fn()}
|
||||
@ -97,6 +99,7 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
card={card}
|
||||
board={board}
|
||||
visiblePropertyTemplates={[propertyTemplate]}
|
||||
visibleBadges={false}
|
||||
isSelected={false}
|
||||
readonly={false}
|
||||
onDrop={jest.fn()}
|
||||
@ -132,6 +135,7 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
card={card}
|
||||
board={board}
|
||||
visiblePropertyTemplates={[propertyTemplate]}
|
||||
visibleBadges={false}
|
||||
isSelected={false}
|
||||
readonly={false}
|
||||
onDrop={jest.fn()}
|
||||
@ -157,6 +161,7 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
card={card}
|
||||
board={board}
|
||||
visiblePropertyTemplates={[propertyTemplate]}
|
||||
visibleBadges={false}
|
||||
isSelected={false}
|
||||
readonly={false}
|
||||
onDrop={jest.fn()}
|
||||
|
@ -25,12 +25,14 @@ import PropertyValueElement from '../propertyValueElement'
|
||||
|
||||
import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox'
|
||||
import './kanbanCard.scss'
|
||||
import CardBadges from '../cardBadges'
|
||||
|
||||
type Props = {
|
||||
card: Card
|
||||
board: Board
|
||||
visiblePropertyTemplates: IPropertyTemplate[]
|
||||
isSelected: boolean
|
||||
visibleBadges: boolean
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
readonly: boolean
|
||||
onDrop: (srcCard: Card, dstCard: Card) => void
|
||||
@ -165,6 +167,7 @@ const KanbanCard = React.memo((props: Props) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
{props.visibleBadges && <CardBadges card={card}/>}
|
||||
</div>
|
||||
|
||||
{showConfirmationDialogBox && <ConfirmationDialogBox dialogBox={confirmDialogProps}/>}
|
||||
|
@ -434,7 +434,9 @@ exports[`components/sidebar/SidebarSettingsMenu languages menu open should match
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Random icons"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -607,7 +609,9 @@ exports[`components/sidebar/SidebarSettingsMenu settings menu open should match
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Random icons"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -870,7 +874,9 @@ exports[`components/sidebar/SidebarSettingsMenu theme menu open should match sna
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Random icons"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
|
@ -25,7 +25,9 @@ exports[`components/viewHeader/filterValue return filterValue 1`] = `
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Status"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
|
@ -30,7 +30,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -100,7 +100,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
|
@ -6,7 +6,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -116,7 +116,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -226,7 +226,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu with Share Boar
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -336,7 +336,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu without Share B
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
|
@ -3,7 +3,7 @@
|
||||
exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -25,7 +25,9 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Status"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -44,7 +46,9 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Property 1"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -63,7 +67,9 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Property 2"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -82,7 +88,9 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Property 3"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -100,6 +108,27 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Comments and Description"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Comments and Description
|
||||
</div>
|
||||
<div
|
||||
class="Switch"
|
||||
>
|
||||
<div
|
||||
class="octo-switch-inner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -134,7 +163,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
|
||||
exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu with gallery typeview 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
@ -156,7 +185,9 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Title"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -175,7 +206,9 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Status"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -194,7 +227,9 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Property 1"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -213,7 +248,9 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Property 2"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -232,7 +269,9 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Property 3"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
@ -250,6 +289,27 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Comments and Description"
|
||||
class="MenuOption SwitchOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Comments and Description
|
||||
</div>
|
||||
<div
|
||||
class="Switch"
|
||||
>
|
||||
<div
|
||||
class="octo-switch-inner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
|
@ -22,6 +22,7 @@ import ViewHeaderActionsMenu from './viewHeaderActionsMenu'
|
||||
|
||||
jest.mock('../../archiver')
|
||||
jest.mock('../../csvExporter')
|
||||
jest.mock('../../mutator')
|
||||
const mockedArchiver = mocked(Archiver, true)
|
||||
const mockedCsvExporter = mocked(CsvExporter, true)
|
||||
|
||||
@ -56,7 +57,7 @@ describe('components/viewHeader/viewHeaderActionsMenu', () => {
|
||||
),
|
||||
)
|
||||
const buttonElement = screen.getByRole('button', {
|
||||
name: 'menuwrapper',
|
||||
name: 'View menu',
|
||||
})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
@ -76,7 +77,7 @@ describe('components/viewHeader/viewHeaderActionsMenu', () => {
|
||||
),
|
||||
)
|
||||
const buttonElement = screen.getByRole('button', {
|
||||
name: 'menuwrapper',
|
||||
name: 'View menu',
|
||||
})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
@ -94,7 +95,7 @@ describe('components/viewHeader/viewHeaderActionsMenu', () => {
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
const buttonElement = screen.getByRole('button', {name: 'View menu'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
const buttonExportCSV = screen.getByRole('button', {name: 'Export to CSV'})
|
||||
@ -115,7 +116,7 @@ describe('components/viewHeader/viewHeaderActionsMenu', () => {
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
const buttonElement = screen.getByRole('button', {name: 'View menu'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
const buttonExportBoardArchive = screen.getByRole('button', {name: 'Export board archive'})
|
||||
|
@ -108,7 +108,7 @@ const ViewHeaderActionsMenu = React.memo((props: Props) => {
|
||||
|
||||
return (
|
||||
<ModalWrapper>
|
||||
<MenuWrapper>
|
||||
<MenuWrapper label={intl.formatMessage({id: 'ViewHeader.view-menu', defaultMessage: 'View menu'})}>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
|
@ -7,14 +7,23 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import mutator from '../../mutator'
|
||||
|
||||
import {mockStateStore, wrapIntl} from '../../testUtils'
|
||||
|
||||
import {Constants} from '../../constants'
|
||||
|
||||
import ViewHeaderPropertiesMenu from './viewHeaderPropertiesMenu'
|
||||
|
||||
jest.mock('../../mutator')
|
||||
const mockedMutator = mocked(mutator, true)
|
||||
|
||||
const board = TestBlockFactory.createBoard()
|
||||
let activeView:BoardView
|
||||
|
||||
@ -42,7 +51,7 @@ describe('components/viewHeader/viewHeaderPropertiesMenu', () => {
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
const buttonElement = screen.getByRole('button', {name: 'Properties menu'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
@ -58,8 +67,29 @@ describe('components/viewHeader/viewHeaderPropertiesMenu', () => {
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
const buttonElement = screen.getByRole('button', {name: 'Properties menu'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('show menu and verify the call for showing card badges', () => {
|
||||
render(
|
||||
wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<ViewHeaderPropertiesMenu
|
||||
activeView={activeView}
|
||||
properties={board.fields.cardProperties}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
const menuButton = screen.getByRole('button', {name: 'Properties menu'})
|
||||
userEvent.click(menuButton)
|
||||
const badgesButton = screen.getByRole('button', {name: 'Comments and Description'})
|
||||
userEvent.click(badgesButton)
|
||||
expect(mockedMutator.changeViewVisibleProperties).toHaveBeenCalledWith(
|
||||
activeView.id,
|
||||
activeView.fields.visiblePropertyIds,
|
||||
[...activeView.fields.visiblePropertyIds, Constants.badgesColumnId],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -18,8 +18,21 @@ type Props = {
|
||||
const ViewHeaderPropertiesMenu = React.memo((props: Props) => {
|
||||
const {properties, activeView} = props
|
||||
const intl = useIntl()
|
||||
const {viewType, visiblePropertyIds} = activeView.fields
|
||||
const canShowBadges = viewType === 'board' || viewType === 'gallery' || viewType === 'calendar'
|
||||
|
||||
const toggleVisibility = (propertyId: string) => {
|
||||
let newVisiblePropertyIds = []
|
||||
if (visiblePropertyIds.includes(propertyId)) {
|
||||
newVisiblePropertyIds = visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
} else {
|
||||
newVisiblePropertyIds = [...visiblePropertyIds, propertyId]
|
||||
}
|
||||
mutator.changeViewVisibleProperties(activeView.id, visiblePropertyIds, newVisiblePropertyIds)
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuWrapper>
|
||||
<MenuWrapper label={intl.formatMessage({id: 'ViewHeader.properties-menu', defaultMessage: 'Properties menu'})}>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.properties'
|
||||
@ -32,34 +45,26 @@ const ViewHeaderPropertiesMenu = React.memo((props: Props) => {
|
||||
key={Constants.titleColumnId}
|
||||
id={Constants.titleColumnId}
|
||||
name={intl.formatMessage({id: 'default-properties.title', defaultMessage: 'Title'})}
|
||||
isOn={activeView.fields.visiblePropertyIds.includes(Constants.titleColumnId)}
|
||||
onClick={(propertyId: string) => {
|
||||
let newVisiblePropertyIds = []
|
||||
if (activeView.fields.visiblePropertyIds.includes(propertyId)) {
|
||||
newVisiblePropertyIds = activeView.fields.visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
} else {
|
||||
newVisiblePropertyIds = [...activeView.fields.visiblePropertyIds, propertyId]
|
||||
}
|
||||
mutator.changeViewVisibleProperties(activeView.id, activeView.fields.visiblePropertyIds, newVisiblePropertyIds)
|
||||
}}
|
||||
isOn={visiblePropertyIds.includes(Constants.titleColumnId)}
|
||||
onClick={toggleVisibility}
|
||||
/>}
|
||||
{properties?.map((option: IPropertyTemplate) => (
|
||||
<Menu.Switch
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
isOn={activeView.fields.visiblePropertyIds.includes(option.id)}
|
||||
onClick={(propertyId: string) => {
|
||||
let newVisiblePropertyIds = []
|
||||
if (activeView.fields.visiblePropertyIds.includes(propertyId)) {
|
||||
newVisiblePropertyIds = activeView.fields.visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
} else {
|
||||
newVisiblePropertyIds = [...activeView.fields.visiblePropertyIds, propertyId]
|
||||
}
|
||||
mutator.changeViewVisibleProperties(activeView.id, activeView.fields.visiblePropertyIds, newVisiblePropertyIds)
|
||||
}}
|
||||
isOn={visiblePropertyIds.includes(option.id)}
|
||||
onClick={toggleVisibility}
|
||||
/>
|
||||
))}
|
||||
{canShowBadges &&
|
||||
<Menu.Switch
|
||||
key={Constants.badgesColumnId}
|
||||
id={Constants.badgesColumnId}
|
||||
name={intl.formatMessage({id: 'default-properties.badges', defaultMessage: 'Comments and Description'})}
|
||||
isOn={visiblePropertyIds.includes(Constants.badgesColumnId)}
|
||||
onClick={toggleVisibility}
|
||||
/>}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
)
|
||||
|
@ -18,6 +18,7 @@ class Constants {
|
||||
static readonly minColumnWidth = 100
|
||||
static readonly defaultTitleColumnWidth = 280
|
||||
static readonly titleColumnId = '__title'
|
||||
static readonly badgesColumnId = '__badges'
|
||||
|
||||
static readonly versionString = '0.14.0'
|
||||
|
||||
|
@ -10,6 +10,8 @@ import {createFilterClause} from '../blocks/filterClause'
|
||||
import {createFilterGroup} from '../blocks/filterGroup'
|
||||
import {ImageBlock, createImageBlock} from '../blocks/imageBlock'
|
||||
import {TextBlock, createTextBlock} from '../blocks/textBlock'
|
||||
import {CheckboxBlock, createCheckboxBlock} from '../blocks/checkboxBlock'
|
||||
import {Block} from '../blocks/block'
|
||||
|
||||
class TestBlockFactory {
|
||||
static createBoard(): Board {
|
||||
@ -121,39 +123,43 @@ class TestBlockFactory {
|
||||
return card
|
||||
}
|
||||
|
||||
static createComment(card: Card): CommentBlock {
|
||||
const block = createCommentBlock()
|
||||
private static addToCard<BlockType extends Block>(block: BlockType, card: Card, isContent = true): BlockType {
|
||||
block.parentId = card.id
|
||||
block.rootId = card.rootId
|
||||
if (isContent) {
|
||||
card.fields.contentOrder.push(block.id)
|
||||
}
|
||||
return block
|
||||
}
|
||||
|
||||
static createComment(card: Card): CommentBlock {
|
||||
const block = this.addToCard(createCommentBlock(), card, false)
|
||||
block.title = 'title'
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
static createText(card: Card): TextBlock {
|
||||
const block = createTextBlock()
|
||||
block.parentId = card.id
|
||||
block.rootId = card.rootId
|
||||
const block = this.addToCard(createTextBlock(), card)
|
||||
block.title = 'title'
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
static createImage(card: Card): ImageBlock {
|
||||
const block = createImageBlock()
|
||||
block.parentId = card.id
|
||||
block.rootId = card.rootId
|
||||
const block = this.addToCard(createImageBlock(), card)
|
||||
block.fields.fileId = 'fileId'
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
static createDivider(card: Card): DividerBlock {
|
||||
const block = createDividerBlock()
|
||||
block.parentId = card.id
|
||||
block.rootId = card.rootId
|
||||
const block = this.addToCard(createDividerBlock(), card)
|
||||
block.title = 'title'
|
||||
return block
|
||||
}
|
||||
|
||||
static createCheckbox(card: Card): CheckboxBlock {
|
||||
const block = this.addToCard(createCheckboxBlock(), card)
|
||||
block.title = 'title'
|
||||
return block
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ import {HTML5Backend} from 'react-dnd-html5-backend'
|
||||
import configureStore, {MockStoreEnhanced} from 'redux-mock-store'
|
||||
import {Middleware} from 'redux'
|
||||
|
||||
import {Block} from './blocks/block'
|
||||
|
||||
export const wrapIntl = (children?: React.ReactNode): JSX.Element => <IntlProvider locale='en'>{children}</IntlProvider>
|
||||
export const wrapDNDIntl = (children?: React.ReactNode): JSX.Element => {
|
||||
return (
|
||||
@ -51,3 +53,12 @@ export function mockStateStore(middleware:Middleware[], state:unknown): MockStor
|
||||
const mockStore = configureStore(middleware)
|
||||
return mockStore(state)
|
||||
}
|
||||
|
||||
export type BlocksById<BlockType> = {[key: string]: BlockType}
|
||||
|
||||
export function blocksById<BlockType extends Block>(blocks: Array<BlockType>): BlocksById<BlockType> {
|
||||
return blocks.reduce((res, block) => {
|
||||
res[block.id] = block
|
||||
return res
|
||||
}, {} as BlocksById<BlockType>)
|
||||
}
|
||||
|
@ -54,6 +54,20 @@ describe('utils', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('countCheckboxesInMarkdown', () => {
|
||||
test('should count checkboxes', () => {
|
||||
const text = `
|
||||
## Header
|
||||
- [x] one
|
||||
- [ ] two
|
||||
- [x] three
|
||||
`.replace(/\n\s+/gm, '\n')
|
||||
const checkboxes = Utils.countCheckboxesInMarkdown(text)
|
||||
expect(checkboxes.total).toBe(3)
|
||||
expect(checkboxes.checked).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('test - buildURL', () => {
|
||||
test('buildURL, no base', () => {
|
||||
expect(Utils.buildURL('test', true)).toBe('http://localhost/test')
|
||||
|
@ -234,10 +234,29 @@ class Utils {
|
||||
return `<div class="table-responsive"><table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table></div>`
|
||||
}
|
||||
|
||||
return this.htmlFromMarkdownWithRenderer(text, renderer)
|
||||
}
|
||||
|
||||
static htmlFromMarkdownWithRenderer(text: string, renderer: marked.Renderer): string {
|
||||
const html = marked(text.replace(/</g, '<'), {renderer, breaks: true})
|
||||
return html.trim()
|
||||
}
|
||||
|
||||
static countCheckboxesInMarkdown(text: string): {total: number, checked: number} {
|
||||
let total = 0
|
||||
let checked = 0
|
||||
const renderer = new marked.Renderer()
|
||||
renderer.checkbox = (isChecked) => {
|
||||
++total
|
||||
if (isChecked) {
|
||||
++checked
|
||||
}
|
||||
return ''
|
||||
}
|
||||
this.htmlFromMarkdownWithRenderer(text, renderer)
|
||||
return {total, checked}
|
||||
}
|
||||
|
||||
// Date and Time
|
||||
private static yearOption(date: Date) {
|
||||
const isCurrentYear = date.getFullYear() === new Date().getFullYear()
|
||||
|
@ -2,6 +2,7 @@
|
||||
stroke: rgba(var(--center-channel-color-rgb), 0.5);
|
||||
stroke-width: 8px;
|
||||
fill: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
15
webapp/src/widgets/icons/message.tsx
Normal file
15
webapp/src/widgets/icons/message.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import CompassIcon from './compassIcon'
|
||||
|
||||
export default function MessageIcon(): JSX.Element {
|
||||
return (
|
||||
<CompassIcon
|
||||
icon='message-text-outline'
|
||||
className='MessageIcon'
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
.TextIcon {
|
||||
fill: rgba(var(--center-channel-color-rgb), 0.7);
|
||||
fill: rgba(var(--center-channel-color-rgb), 0.5);
|
||||
stroke: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ function SwitchOption(props: SwitchOptionProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className='MenuOption SwitchOption menu-option'
|
||||
role='button'
|
||||
aria-label={name}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.target.dispatchEvent(new Event('menuItemClicked'))
|
||||
props.onClick(props.id)
|
||||
|
@ -11,6 +11,7 @@ type Props = {
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
isOpen?: boolean
|
||||
label?: string
|
||||
}
|
||||
|
||||
const MenuWrapper = React.memo((props: Props) => {
|
||||
@ -84,7 +85,7 @@ const MenuWrapper = React.memo((props: Props) => {
|
||||
return (
|
||||
<div
|
||||
role='button'
|
||||
aria-label='menuwrapper'
|
||||
aria-label={props.label || 'menuwrapper'}
|
||||
className={className}
|
||||
onClick={toggle}
|
||||
ref={node}
|
||||
|
Loading…
x
Reference in New Issue
Block a user