1
0
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:
kamre 2022-01-13 23:26:27 +07:00 committed by GitHub
parent b33b998f9e
commit 342c8df39d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 672 additions and 94 deletions

View File

@ -5,7 +5,8 @@
"**/login*.ts",
"**/create*.ts",
"**/manage*.ts",
"**/group*.ts"
"**/group*.ts",
"**/card*.ts"
],
"env": {
"username": "test-user",

View File

@ -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.

View 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()
}
})

View File

@ -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)
}
})

View 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 />`;

View File

@ -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"
>

View File

@ -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"
>

View File

@ -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>
)
}

View File

@ -0,0 +1,13 @@
.CardBadges {
display: flex;
align-items: center;
span {
margin-right: 8px;
.Icon,
.CompassIcon {
margin-right: 4px;
}
}
}

View 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')
})
})

View 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)

View File

@ -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"

View File

@ -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: {},

View File

@ -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}

View File

@ -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;
}
}

View File

@ -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()}

View File

@ -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>
)
})

View File

@ -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"

View File

@ -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)}

View File

@ -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()}

View File

@ -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}/>}

View File

@ -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"

View File

@ -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"

View File

@ -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"
>

View File

@ -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"
>

View File

@ -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"

View File

@ -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'})

View File

@ -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

View File

@ -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],
)
})
})

View File

@ -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>
)

View File

@ -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'

View File

@ -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
}
}

View File

@ -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>)
}

View File

@ -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')

View File

@ -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, '&lt;'), {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()

View File

@ -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;
}

View 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'
/>
)
}

View File

@ -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;
}

View File

@ -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)

View File

@ -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}