Migrating everything to 1 single editable type
This commit is contained in:
parent
771be97c3e
commit
2b9b5cb07f
6 changed files with 95 additions and 305 deletions
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue