`;
-exports[`components/content/CheckboxElement should match snapshot on toggle 1`] = `
+exports[`components/content/checkboxElement should toggle value 1`] = `
{
- const defaultBlock: ContentBlock = {
- id: 'test-id',
- workspaceId: '',
- parentId: '',
- rootId: '',
- modifiedBy: 'test-user-id',
- schema: 0,
- type: 'checkbox',
- title: 'test-title',
- fields: {},
- createdBy: 'test-user-id',
- createAt: 0,
- updateAt: 0,
- deleteAt: 0,
- }
+const board = TestBlockFactory.createBoard()
+const card = TestBlockFactory.createCard(board)
+const checkboxBlock: ContentBlock = {
+ id: 'test-id',
+ workspaceId: '',
+ parentId: card.id,
+ rootId: card.rootId,
+ modifiedBy: 'test-user-id',
+ schema: 1,
+ type: 'checkbox',
+ title: 'test-title',
+ fields: {value: false},
+ createdBy: 'test-user-id',
+ createAt: 0,
+ updateAt: 0,
+ deleteAt: 0,
+}
- beforeAll(() => {
- fetchMock.post('*', {})
- })
+const cardDetailContextValue = (autoAdded: boolean): CardDetailContextType => ({
+ card,
+ lastAddedBlock: {
+ id: checkboxBlock.id,
+ autoAdded,
+ },
+ deleteBlock: jest.fn(),
+ addBlock: jest.fn(),
+})
- afterAll(() => {
- fetchMock.mockClear()
- })
+const wrap = (child: ReactNode): ReactElement => (
+ wrapIntl(
+
+ {child}
+ ,
+ )
+)
- test('should match snapshot', () => {
- const component = wrapIntl(
+describe('components/content/checkboxElement', () => {
+ beforeEach(jest.clearAllMocks)
+
+ it('should match snapshot', () => {
+ const component = wrap(
,
)
@@ -49,10 +68,10 @@ describe('components/content/CheckboxElement', () => {
expect(container).toMatchSnapshot()
})
- test('should match snapshot on read only', () => {
- const component = wrapIntl(
+ it('should match snapshot when read only', () => {
+ const component = wrap(
,
)
@@ -60,29 +79,101 @@ describe('components/content/CheckboxElement', () => {
expect(container).toMatchSnapshot()
})
- test('should match snapshot on change title', () => {
- const component = wrapIntl(
+ it('should change title', () => {
+ const {container} = render(wrap(
,
- )
- const {container, getByTitle} = render(component)
- const input = getByTitle(/test-title/i)
- fireEvent.blur(input, {target: {textContent: 'changed name'}})
+ ))
+ const newTitle = 'new title'
+ const input = screen.getByRole('textbox', {name: /test-title/i})
+ userEvent.clear(input)
+ userEvent.type(input, newTitle)
+ fireEvent.blur(input)
expect(container).toMatchSnapshot()
+ expect(mockedMutator.updateBlock).toHaveBeenCalledTimes(1)
+ expect(mockedMutator.updateBlock).toHaveBeenCalledWith(
+ expect.objectContaining({title: newTitle}),
+ checkboxBlock,
+ expect.anything())
})
- test('should match snapshot on toggle', () => {
- const component = wrapIntl(
+ it('should toggle value', () => {
+ const {container} = render(wrap(
,
- )
- const {container, getByRole} = render(component)
- const input = getByRole('checkbox')
- fireEvent.change(input, {target: {value: 'on'}})
+ ))
+ const input = screen.getByRole('checkbox')
+ userEvent.click(input)
expect(container).toMatchSnapshot()
+ expect(mockedMutator.updateBlock).toHaveBeenCalledTimes(1)
+ expect(mockedMutator.updateBlock).toHaveBeenCalledWith(
+ expect.objectContaining({fields: {value: true}}),
+ checkboxBlock,
+ expect.anything())
+ })
+
+ it('should have focus when last added', () => {
+ render(wrapIntl(
+
+
+ ,
+ ))
+ const input = screen.getByRole('textbox', {name: /test-title/i})
+ expect(input).toHaveFocus()
+ })
+
+ it('should add new checkbox when enter pressed', async () => {
+ const addElement = jest.fn()
+ render(wrap(
+ ,
+ ))
+ const input = screen.getByRole('textbox', {name: /test-title/i})
+
+ // should not add new checkbox when current one has empty title
+ userEvent.clear(input)
+ userEvent.type(input, '{enter}')
+ expect(addElement).toHaveBeenCalledTimes(0)
+
+ // should add new checkbox when current one has non-empty title
+ userEvent.clear(input)
+ userEvent.type(input, 'new-title{enter}')
+ await waitFor(() => expect(addElement).toHaveBeenCalledTimes(1))
+ })
+
+ it('should delete automatically added checkbox with empty title on esc/enter pressed', () => {
+ const addedBlock = createContentBlock(checkboxBlock)
+ addedBlock.title = ''
+ const deleteElement = jest.fn()
+
+ render(wrapIntl(
+
+
+ ,
+ ))
+
+ const input = screen.getByRole('textbox')
+ userEvent.type(input, '{esc}')
+ expect(deleteElement).toHaveBeenCalledTimes(1)
+ userEvent.type(input, '{enter}')
+ expect(deleteElement).toHaveBeenCalledTimes(2)
+
+ // should not delete if title is not empty
+ userEvent.type(input, 'new-title{esc}')
+ expect(deleteElement).toHaveBeenCalledTimes(2)
})
})
diff --git a/webapp/src/components/content/checkboxElement.tsx b/webapp/src/components/content/checkboxElement.tsx
index 953160e13..29afb4044 100644
--- a/webapp/src/components/content/checkboxElement.tsx
+++ b/webapp/src/components/content/checkboxElement.tsx
@@ -1,25 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React, {useState} from 'react'
+import React, {useEffect, useRef, useState} from 'react'
import {useIntl} from 'react-intl'
import {createCheckboxBlock} from '../../blocks/checkboxBlock'
import {ContentBlock} from '../../blocks/contentBlock'
import CheckIcon from '../../widgets/icons/check'
import mutator from '../../mutator'
-import Editable from '../../widgets/editable'
+import Editable, {Focusable} from '../../widgets/editable'
+import {useCardDetailContext} from '../cardDetail/cardDetailContext'
import {contentRegistry} from './contentRegistry'
+
import './checkboxElement.scss'
type Props = {
block: ContentBlock
readonly: boolean
+ onAddElement?: () => void
+ onDeleteElement?: () => void
}
const CheckboxElement = React.memo((props: Props) => {
const {block, readonly} = props
const intl = useIntl()
+ const titleRef = useRef(null)
+ const cardDetail = useCardDetailContext()
+ const [addedBlockId, setAddedBlockId] = useState(cardDetail.lastAddedBlock.id)
+
+ useEffect(() => {
+ if (block.id === addedBlockId) {
+ titleRef.current?.focus()
+ setAddedBlockId('')
+ }
+ }, [block, addedBlockId, titleRef])
const [active, setActive] = useState(Boolean(block.fields.value))
const [title, setTitle] = useState(block.title)
@@ -42,14 +56,24 @@ const CheckboxElement = React.memo((props: Props) => {
}}
/>
{
- const newBlock = createCheckboxBlock(block)
- newBlock.title = title
- newBlock.fields.value = active
- mutator.updateBlock(newBlock, block, intl.formatMessage({id: 'ContentBlock.editCardCheckboxText', defaultMessage: 'edit card text'}))
+ saveOnEsc={true}
+ onSave={async (saveType) => {
+ const {lastAddedBlock} = cardDetail
+ if (title === '' && block.id === lastAddedBlock.id && lastAddedBlock.autoAdded && props.onDeleteElement) {
+ props.onDeleteElement()
+ } else {
+ const newBlock = createCheckboxBlock(block)
+ newBlock.title = title
+ newBlock.fields.value = active
+ await mutator.updateBlock(newBlock, block, intl.formatMessage({id: 'ContentBlock.editCardCheckboxText', defaultMessage: 'edit card text'}))
+ if (saveType === 'onEnter' && title !== '' && props.onAddElement) {
+ props.onAddElement()
+ }
+ }
}}
readonly={readonly}
spellCheck={true}
@@ -65,11 +89,13 @@ contentRegistry.registerContentType({
createBlock: async () => {
return createCheckboxBlock()
},
- createComponent: (block, readonly) => {
+ createComponent: (block, readonly, onAddElement, onDeleteElement) => {
return (
)
},
diff --git a/webapp/src/components/content/contentElement.tsx b/webapp/src/components/content/contentElement.tsx
index e6ca62493..306442a04 100644
--- a/webapp/src/components/content/contentElement.tsx
+++ b/webapp/src/components/content/contentElement.tsx
@@ -1,9 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import {useCallback} from 'react'
+
import {ContentBlock} from '../../blocks/contentBlock'
import {Utils} from '../../utils'
+import {useCardDetailContext} from '../cardDetail/cardDetailContext'
+
import {contentRegistry} from './contentRegistry'
// Need to require here to prevent webpack from tree-shaking these away
@@ -16,10 +20,12 @@ import './checkboxElement'
type Props = {
block: ContentBlock
readonly: boolean
+ cords: {x: number, y?: number, z?: number}
}
export default function ContentElement(props: Props): JSX.Element|null {
- const {block, readonly} = props
+ const {block, readonly, cords} = props
+ const cardDetail = useCardDetailContext()
const handler = contentRegistry.getHandler(block.type)
if (!handler) {
@@ -27,5 +33,15 @@ export default function ContentElement(props: Props): JSX.Element|null {
return null
}
- return handler.createComponent(block, readonly)
+ const addElement = useCallback(() => {
+ const index = cords.x + 1
+ cardDetail.addBlock(handler, index, true)
+ }, [cardDetail, cords, handler])
+
+ const deleteElement = useCallback(() => {
+ const index = cords.x
+ cardDetail.deleteBlock(block, index)
+ }, [block, cords, cardDetail])
+
+ return handler.createComponent(block, readonly, addElement, deleteElement)
}
diff --git a/webapp/src/components/content/contentRegistry.tsx b/webapp/src/components/content/contentRegistry.tsx
index ae81cca2b..83882fc87 100644
--- a/webapp/src/components/content/contentRegistry.tsx
+++ b/webapp/src/components/content/contentRegistry.tsx
@@ -7,12 +7,12 @@ import {BlockTypes} from '../../blocks/block'
import {ContentBlock} from '../../blocks/contentBlock'
import {Utils} from '../../utils'
-type ContentHandler = {
+export type ContentHandler = {
type: BlockTypes,
getDisplayText: (intl: IntlShape) => string,
getIcon: () => JSX.Element,
createBlock: (rootId: string) => Promise,
- createComponent: (block: ContentBlock, readonly: boolean) => JSX.Element,
+ createComponent: (block: ContentBlock, readonly: boolean, onAddElement?: () => void, onDeleteElement?: () => void) => JSX.Element,
}
class ContentRegistry {
@@ -41,6 +41,4 @@ class ContentRegistry {
const contentRegistry = new ContentRegistry()
-export type {ContentHandler}
export {contentRegistry}
-
diff --git a/webapp/src/components/contentBlock.tsx b/webapp/src/components/contentBlock.tsx
index c64e77e78..718d2e81e 100644
--- a/webapp/src/components/contentBlock.tsx
+++ b/webapp/src/components/contentBlock.tsx
@@ -149,6 +149,7 @@ const ContentBlock = React.memo((props: Props): JSX.Element => {
{
key={b.id}
block={b}
readonly={true}
+ cords={{x: 0}}
/>
))
}
@@ -137,6 +138,7 @@ const GalleryCard = React.memo((props: Props) => {
key={block.id}
block={block}
readonly={true}
+ cords={{x: 0}}
/>
)
})}