Migrating everything to 1 single editable type

This commit is contained in:
Jesús Espino 2021-03-30 13:11:56 +02:00
parent 771be97c3e
commit 2b9b5cb07f
6 changed files with 95 additions and 305 deletions

View file

@ -1,168 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {Utils} from '../utils'
type Props = {
onChanged: (text: string) => void
text?: string
placeholderText?: string
className?: string
style?: React.CSSProperties
isMarkdown: boolean
isMultiline: boolean
allowEmpty: boolean
readonly?: boolean
onFocus?: () => void
onBlur?: () => void
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void
}
class Editable extends React.PureComponent<Props> {
static defaultProps = {
text: '',
isMarkdown: false,
isMultiline: false,
allowEmpty: true,
}
private privateText = ''
get text(): string {
return this.privateText
}
set text(value: string) {
if (!this.elementRef.current) {
Utils.assertFailure('elementRef.current')
return
}
const {isMarkdown} = this.props
if (value) {
this.elementRef.current.innerHTML = isMarkdown ? Utils.htmlFromMarkdown(value) : Utils.htmlEncode(value)
} else {
this.elementRef.current.innerText = ''
}
this.privateText = value || ''
}
private elementRef = React.createRef<HTMLDivElement>()
constructor(props: Props) {
super(props)
this.privateText = props.text || ''
}
componentDidUpdate(): void {
this.privateText = this.props.text || ''
}
focus(): void {
this.elementRef.current?.focus()
// Put cursor at end
document.execCommand('selectAll', false, undefined)
document.getSelection()?.collapseToEnd()
}
blur(): void {
this.elementRef.current?.blur()
}
render(): JSX.Element {
const {text, style, placeholderText, isMarkdown, isMultiline, onFocus, onBlur, onKeyDown, onChanged} = this.props
const initialStyle = {...this.props.style}
let html: string
if (text) {
html = isMarkdown ? Utils.htmlFromMarkdown(text) : Utils.htmlEncode(text)
} else {
html = ''
}
let className = 'octo-editable'
if (this.props.className) {
className += ' ' + this.props.className
}
const element = (
<div
ref={this.elementRef}
className={className}
contentEditable={!this.props.readonly}
suppressContentEditableWarning={true}
style={initialStyle}
placeholder={placeholderText}
dangerouslySetInnerHTML={{__html: html}}
onFocus={() => {
if (this.props.readonly) {
return
}
if (this.elementRef.current) {
this.elementRef.current.innerText = this.text
this.elementRef.current.style.color = style?.color || ''
this.elementRef.current.classList.add('active')
}
if (onFocus) {
onFocus()
}
}}
onBlur={async () => {
if (this.props.readonly) {
return
}
if (this.elementRef.current) {
const newText = this.elementRef.current.innerText
const oldText = this.props.text || ''
if (this.props.allowEmpty || newText) {
if (newText !== oldText && onChanged) {
onChanged(newText)
}
this.text = newText
} else {
this.text = oldText // Reset text
}
this.elementRef.current.classList.remove('active')
}
if (onBlur) {
onBlur()
}
}}
onKeyDown={(e) => {
if (this.props.readonly) {
return
}
if (e.keyCode === 27 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // ESC
e.stopPropagation()
this.elementRef.current?.blur()
} else if (!isMultiline && e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // Return
e.stopPropagation()
this.elementRef.current?.blur()
}
if (onKeyDown) {
onKeyDown(e)
}
}}
/>)
return element
}
}
export default Editable

View file

@ -177,7 +177,7 @@ class Kanban extends React.Component<Props> {
)
}
private async propertyNameChanged(option: IPropertyOption, text: string): Promise<void> {
private propertyNameChanged = async (option: IPropertyOption, text: string): Promise<void> => {
const {boardTree} = this.props
await mutator.changePropertyOptionValue(boardTree, boardTree.groupByProperty!, option, text)

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import React from 'react'
import React, {useState, useEffect} from 'react'
import {FormattedMessage, IntlShape} from 'react-intl'
import {Constants} from '../../constants'
@ -16,8 +16,7 @@ import HideIcon from '../../widgets/icons/hide'
import OptionsIcon from '../../widgets/icons/options'
import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import Editable from '../editable'
import Editable from '../../widgets/editable'
type Props = {
boardTree: BoardTree
@ -33,86 +32,16 @@ type Props = {
export default function KanbanColumnHeader(props: Props): JSX.Element {
const {boardTree, intl, group} = props
const {activeView} = boardTree
const [groupTitle, setGroupTitle] = useState(group.option.value)
if (!group.option.id) {
// Empty group
const ref = React.createRef<HTMLDivElement>()
return (
<div
key='empty'
ref={ref}
className='octo-board-header-cell KanbanColumnHeader'
draggable={!props.readonly}
onDragStart={() => {
props.setDraggedHeaderOption(group.option)
}}
onDragEnd={() => {
props.setDraggedHeaderOption(undefined)
}}
onDragOver={(e) => {
ref.current?.classList.add('dragover')
e.preventDefault()
}}
onDragEnter={(e) => {
ref.current?.classList.add('dragover')
e.preventDefault()
}}
onDragLeave={(e) => {
ref.current?.classList.remove('dragover')
e.preventDefault()
}}
onDrop={(e) => {
ref.current?.classList.remove('dragover')
e.preventDefault()
props.onDropToColumn(group.option)
}}
>
<div
className='octo-label'
title={intl.formatMessage({
id: 'BoardComponent.no-property-title',
defaultMessage: 'Items with an empty {property} property will go here. This column cannot be removed.',
}, {property: boardTree.groupByProperty!.name})}
>
<FormattedMessage
id='BoardComponent.no-property'
defaultMessage='No {property}'
values={{
property: boardTree.groupByProperty!.name,
}}
/>
</div>
<Button>{`${group.cards.length}`}</Button>
<div className='octo-spacer'/>
{!props.readonly &&
<>
<MenuWrapper>
<IconButton icon={<OptionsIcon/>}/>
<Menu>
<Menu.Text
id='hide'
icon={<HideIcon/>}
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
onClick={() => mutator.hideViewColumn(activeView, '')}
/>
</Menu>
</MenuWrapper>
<IconButton
icon={<AddIcon/>}
onClick={() => props.addCard(undefined)}
/>
</>
}
</div>
)
}
useEffect(() => {
setGroupTitle(group.option.value)
}, [group.option.value])
const ref = React.createRef<HTMLDivElement>()
return (
<div
key={group.option.id}
key={group.option.id || 'empty'}
ref={ref}
className='octo-board-header-cell KanbanColumnHeader'
@ -142,16 +71,39 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
props.onDropToColumn(group.option)
}}
>
<Editable
className={`octo-label ${group.option.color}`}
text={group.option.value}
placeholderText='New Select'
allowEmpty={false}
onChanged={(text) => {
props.propertyNameChanged(group.option, text)
}}
readonly={props.readonly}
/>
{!group.option.id &&
<div
className='octo-label'
title={intl.formatMessage({
id: 'BoardComponent.no-property-title',
defaultMessage: 'Items with an empty {property} property will go here. This column cannot be removed.',
}, {property: boardTree.groupByProperty!.name})}
>
<FormattedMessage
id='BoardComponent.no-property'
defaultMessage='No {property}'
values={{
property: boardTree.groupByProperty!.name,
}}
/>
</div>}
{group.option.id &&
<Editable
className={`octo-label ${group.option.color}`}
value={groupTitle}
placeholderText='New Select'
onChange={setGroupTitle}
onSave={() => {
if(groupTitle.trim() === '') {
setGroupTitle(group.option.value)
}
props.propertyNameChanged(group.option, groupTitle)
}}
onCancel={() => {
setGroupTitle(group.option.value)
}}
readonly={props.readonly}
/>}
<Button>{`${group.cards.length}`}</Button>
<div className='octo-spacer'/>
{!props.readonly &&
@ -163,23 +115,26 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
id='hide'
icon={<HideIcon/>}
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
onClick={() => mutator.hideViewColumn(activeView, group.option.id)}
onClick={() => mutator.hideViewColumn(activeView, group.option.id || '')}
/>
<Menu.Text
id='delete'
icon={<DeleteIcon/>}
name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})}
onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty!, group.option)}
/>
<Menu.Separator/>
{Constants.menuColors.map((color) => (
<Menu.Color
key={color.id}
id={color.id}
name={color.name}
onClick={() => mutator.changePropertyOptionColor(boardTree.board, boardTree.groupByProperty!, group.option, color.id)}
/>
))}
{group.option.id &&
<>
<Menu.Text
id='delete'
icon={<DeleteIcon/>}
name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})}
onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty!, group.option)}
/>
<Menu.Separator/>
{Constants.menuColors.map((color) => (
<Menu.Color
key={color.id}
id={color.id}
name={color.name}
onClick={() => mutator.changePropertyOptionColor(boardTree.board, boardTree.groupByProperty!, group.option, color.id)}
/>
))}
</>}
</Menu>
</MenuWrapper>
<IconButton

View file

@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react'
import React, {useState, useEffect} from 'react'
import {FormattedMessage} from 'react-intl'
import ViewMenu from '../../components/viewMenu'
@ -10,8 +10,8 @@ import Button from '../../widgets/buttons/button'
import IconButton from '../../widgets/buttons/iconButton'
import DropdownIcon from '../../widgets/icons/dropdown'
import MenuWrapper from '../../widgets/menuWrapper'
import Editable from '../../widgets/editable'
import Editable from '../editable'
import ModalWrapper from '../modalWrapper'
import NewCardButton from './newCardButton'
@ -42,17 +42,26 @@ const ViewHeader = React.memo((props: Props) => {
const {boardTree, showView, withGroupBy} = props
const {board, activeView} = boardTree
const [viewTitle, setViewTitle] = useState(activeView.title)
useEffect(() => {
setViewTitle(activeView.title)
}, [activeView.title])
const hasFilter = activeView.filter && activeView.filter.filters?.length > 0
return (
<div className='ViewHeader'>
<Editable
style={{color: 'rgb(var(--main-fg))', fontWeight: 600}}
text={activeView.title}
value={viewTitle}
placeholderText='Untitled View'
onChanged={(text) => {
mutator.changeTitle(activeView, text)
onSave={(): void => {
mutator.changeTitle(activeView, viewTitle)
}}
onCancel={(): void => {
setViewTitle(activeView.title)
}}
onChange={setViewTitle}
readonly={props.readonly}
/>
<MenuWrapper>

View file

@ -5,8 +5,7 @@ import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
import {BoardTree} from '../../viewModel/boardTree'
import Button from '../../widgets/buttons/button'
import Editable from '../editable'
import Editable from '../../widgets/editable'
type Props = {
boardTree: BoardTree
@ -15,41 +14,37 @@ type Props = {
}
const ViewHeaderSearch = React.memo((props: Props) => {
const {boardTree, intl} = props
const searchFieldRef = useRef<Editable>(null)
const [isSearching, setIsSearching] = useState(Boolean(props.boardTree.getSearchText()))
const [searchValue, setSearchValue] = useState(boardTree.getSearchText())
useEffect(() => {
searchFieldRef.current?.focus()
}, [isSearching])
const onSearchKeyDown = (e: React.KeyboardEvent) => {
if (e.keyCode === 27) { // ESC: Clear search
if (searchFieldRef.current) {
searchFieldRef.current.text = ''
}
setIsSearching(false)
props.setSearchText(undefined)
e.preventDefault()
}
if (e.keyCode === 13 && searchFieldRef.current?.text.trim() === '') { // ENTER: with empty string clear search
setIsSearching(false)
props.setSearchText(undefined)
e.preventDefault()
}
}
const {boardTree, intl} = props
useEffect(() => {
setSearchValue(boardTree.getSearchText())
}, [boardTree])
if (isSearching) {
return (
<Editable
ref={searchFieldRef}
text={boardTree.getSearchText()}
value={searchValue}
placeholderText={intl.formatMessage({id: 'ViewHeader.search-text', defaultMessage: 'Search text'})}
style={{color: 'rgb(var(--main-fg))'}}
onChanged={props.setSearchText}
onKeyDown={(e) => {
onSearchKeyDown(e)
onChange={setSearchValue}
onCancel={() => {
setSearchValue('')
setIsSearching(false)
props.setSearchText('')
}}
onSave={() => {
if (searchValue === '') {
setIsSearching(false)
}
props.setSearchText(searchValue)
}}
/>
)

View file

@ -5,7 +5,6 @@
text-align: center;
justify-content: center;
border-radius: var(--default-rad);
padding: 4px 8px;
min-width: 20px;
cursor: pointer;
overflow: hidden;
@ -36,6 +35,6 @@
.Icon {
width: 14px;
height: 14px;
margin-right: 5px;
margin-right: 0px;
}
}