Create template from card

This commit is contained in:
Chen-I Lim 2020-11-11 09:21:16 -08:00
parent ca1c46dbab
commit 57d7eb35bd
9 changed files with 174 additions and 133 deletions

View file

@ -9,7 +9,7 @@ interface Card extends IBlock {
readonly icon: string
readonly isTemplate: boolean
readonly properties: Readonly<Record<string, string>>
newCardFromTemplate(): MutableCard
duplicate(): MutableCard
}
class MutableCard extends MutableBlock {
@ -41,11 +41,9 @@ class MutableCard extends MutableBlock {
this.properties = {...(block.fields?.properties || {})}
}
newCardFromTemplate(): MutableCard {
duplicate(): MutableCard {
const card = new MutableCard(this)
card.id = Utils.createGuid()
card.isTemplate = false
card.title = ''
return card
}
}

View file

@ -43,7 +43,7 @@ type Props = {
type State = {
isSearching: boolean
shownCard?: Card
shownCardId?: string
viewMenu: boolean
selectedCardIds: string[]
showFilter: boolean
@ -131,12 +131,14 @@ class BoardComponent extends React.Component<Props, State> {
this.backgroundClicked(e)
}}
>
{this.state.shownCard &&
{this.state.shownCardId &&
<RootPortal>
<CardDialog
key={this.state.shownCardId}
boardTree={boardTree}
card={this.state.shownCard}
onClose={() => this.setState({shownCard: undefined})}
cardId={this.state.shownCardId}
onClose={() => this.setState({shownCardId: undefined})}
showCard={(cardId) => this.setState({shownCardId: cardId})}
/>
</RootPortal>}
@ -155,7 +157,6 @@ class BoardComponent extends React.Component<Props, State> {
addCardFromTemplate={this.addCardFromTemplate}
addCardTemplate={() => this.addCardTemplate()}
editCardTemplate={this.editCardTemplate}
deleteCardTemplate={this.deleteCardTemplate}
withGroupBy={true}
/>
<div
@ -479,21 +480,23 @@ class BoardComponent extends React.Component<Props, State> {
}
}
private addCardFromTemplate = async (cardTemplate?: Card) => {
this.addCard(undefined, cardTemplate)
private addCardFromTemplate = async (cardTemplateId?: string) => {
this.addCard(undefined, cardTemplateId)
}
private async addCard(groupByOptionId?: string, cardTemplate?: Card): Promise<void> {
private async addCard(groupByOptionId?: string, cardTemplateId?: string): Promise<void> {
const {boardTree} = this.props
const {activeView, board} = boardTree
let card: MutableCard
let blocksToInsert: IBlock[]
if (cardTemplate) {
const templateCardTree = new MutableCardTree(cardTemplate.id)
if (cardTemplateId) {
const templateCardTree = new MutableCardTree(cardTemplateId)
await templateCardTree.sync()
const newCardTree = templateCardTree.duplicateFromTemplate()
const newCardTree = templateCardTree.templateCopy()
card = newCardTree.card
card.isTemplate = false
card.title = ''
blocksToInsert = [newCardTree.card, ...newCardTree.contents]
} else {
card = new MutableCard()
@ -515,10 +518,10 @@ class BoardComponent extends React.Component<Props, State> {
blocksToInsert,
'add card',
async () => {
this.setState({shownCard: card})
this.setState({shownCardId: card.id})
},
async () => {
this.setState({shownCard: undefined})
this.setState({shownCardId: undefined})
},
)
}
@ -539,18 +542,14 @@ class BoardComponent extends React.Component<Props, State> {
}
}
await mutator.insertBlock(cardTemplate, 'add card template', async () => {
this.setState({shownCard: cardTemplate})
this.setState({shownCardId: cardTemplate.id})
}, async () => {
this.setState({shownCard: undefined})
this.setState({shownCardId: undefined})
})
}
private editCardTemplate = (cardTemplate: Card) => {
this.setState({shownCard: cardTemplate})
}
private deleteCardTemplate = (cardTemplate: Card) => {
mutator.deleteBlock(cardTemplate, 'delete card template')
private editCardTemplate = (cardTemplateId: string) => {
this.setState({shownCardId: cardTemplateId})
}
private async propertyNameChanged(option: IPropertyOption, text: string): Promise<void> {
@ -586,7 +585,7 @@ class BoardComponent extends React.Component<Props, State> {
this.setState({selectedCardIds})
}
} else {
this.setState({selectedCardIds: [], shownCard: card})
this.setState({selectedCardIds: [], shownCardId: card.id})
}
e.stopPropagation()

View file

@ -7,9 +7,8 @@ import {BlockIcons} from '../blockIcons'
import {MutableTextBlock} from '../blocks/textBlock'
import {BoardTree} from '../viewModel/boardTree'
import {PropertyType} from '../blocks/board'
import {CardTree, MutableCardTree} from '../viewModel/cardTree'
import {CardTree} from '../viewModel/cardTree'
import mutator from '../mutator'
import {OctoListener} from '../octoListener'
import {Utils} from '../utils'
import MenuWrapper from '../widgets/menuWrapper'
@ -29,18 +28,16 @@ import './cardDetail.scss'
type Props = {
boardTree: BoardTree
cardId: string
cardTree: CardTree
intl: IntlShape
}
type State = {
cardTree?: CardTree
title: string
}
class CardDetail extends React.Component<Props, State> {
private titleRef = React.createRef<Editable>()
private cardListener?: OctoListener
shouldComponentUpdate() {
return true
@ -49,54 +46,12 @@ class CardDetail extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
title: '',
title: props.cardTree.card.title,
}
}
componentDidMount() {
this.createCardTreeAndSync()
}
private async createCardTreeAndSync() {
const cardTree = new MutableCardTree(this.props.cardId)
await cardTree.sync()
this.createListener()
this.setState({cardTree, title: cardTree.card.title})
setTimeout(() => {
if (this.titleRef.current) {
this.titleRef.current.focus()
}
}, 0)
}
private createListener() {
this.cardListener = new OctoListener()
this.cardListener.open(
[this.props.cardId],
async (blocks) => {
Utils.log(`cardListener.onChanged: ${blocks.length}`)
const newCardTree = this.state.cardTree.mutableCopy()
if (newCardTree.incrementalUpdate(blocks)) {
this.setState({cardTree: newCardTree, title: newCardTree.card.title})
}
},
async () => {
Utils.log('cardListener.onReconnect')
const newCardTree = this.state.cardTree.mutableCopy()
await newCardTree.sync()
this.setState({cardTree: newCardTree, title: newCardTree.card.title})
},
)
}
componentWillUnmount() {
this.cardListener?.close()
this.cardListener = undefined
}
render() {
const {boardTree, intl} = this.props
const {cardTree} = this.state
const {boardTree, cardTree, intl} = this.props
const {board} = boardTree
if (!cardTree) {
return null
@ -128,7 +83,7 @@ class CardDetail extends React.Component<Props, State> {
const block = new MutableTextBlock()
block.parentId = card.id
block.title = text
block.order = (this.state.cardTree.contents.length + 1) * 1000
block.order = (this.props.cardTree.contents.length + 1) * 1000
mutator.insertBlock(block, 'add card text')
}
}}
@ -170,11 +125,11 @@ class CardDetail extends React.Component<Props, State> {
onChange={(title: string) => this.setState({title})}
saveOnEsc={true}
onSave={() => {
if (this.state.title !== this.state.cardTree.card.title) {
if (this.state.title !== this.props.cardTree.card.title) {
mutator.changeTitle(card, this.state.title)
}
}}
onCancel={() => this.setState({title: this.state.cardTree.card.title})}
onCancel={() => this.setState({title: this.props.cardTree.card.title})}
/>
{/* Property list */}
@ -254,7 +209,7 @@ class CardDetail extends React.Component<Props, State> {
onClick={() => {
const block = new MutableTextBlock()
block.parentId = card.id
block.order = (this.state.cardTree.contents.length + 1) * 1000
block.order = (this.props.cardTree.contents.length + 1) * 1000
mutator.insertBlock(block, 'add text')
}}
/>
@ -262,7 +217,7 @@ class CardDetail extends React.Component<Props, State> {
id='image'
name={intl.formatMessage({id: 'CardDetail.image', defaultMessage: 'Image'})}
onClick={() => Utils.selectLocalFile(
(file) => mutator.createImageBlock(card.id, file, (this.state.cardTree.contents.length + 1) * 1000),
(file) => mutator.createImageBlock(card.id, file, (this.props.cardTree.contents.length + 1) * 1000),
'.jpg,.jpeg,.png',
)}
/>

View file

@ -4,23 +4,79 @@ import React from 'react'
import {FormattedMessage} from 'react-intl'
import {Card} from '../blocks/card'
import {BoardTree} from '../viewModel/boardTree'
import mutator from '../mutator'
import Menu from '../widgets/menu'
import DeleteIcon from '../widgets/icons/delete'
import CardDetail from './cardDetail'
import {MutableCardTree} from '../viewModel/cardTree'
import {CardTree} from '../viewModel/cardTree'
import {OctoListener} from '../octoListener'
import {Utils} from '../utils'
import Dialog from './dialog'
import CardDetail from './cardDetail'
type Props = {
boardTree: BoardTree
card: Card
cardId: string
onClose: () => void
showCard: (cardId?: string) => void
}
class CardDialog extends React.Component<Props> {
type State = {
cardTree?: CardTree
}
class CardDialog extends React.Component<Props, State> {
state: State = {}
private cardListener?: OctoListener
shouldComponentUpdate() {
return true
}
componentDidMount() {
this.createCardTreeAndSync()
}
private async createCardTreeAndSync() {
const cardTree = new MutableCardTree(this.props.cardId)
await cardTree.sync()
this.createListener()
this.setState({cardTree})
Utils.log(`cardDialog.createCardTreeAndSync: ${cardTree.card.id}`)
}
private createListener() {
this.cardListener = new OctoListener()
this.cardListener.open(
[this.props.cardId],
async (blocks) => {
Utils.log(`cardListener.onChanged: ${blocks.length}`)
const newCardTree = this.state.cardTree.mutableCopy()
if (newCardTree.incrementalUpdate(blocks)) {
this.setState({cardTree: newCardTree})
}
},
async () => {
Utils.log('cardListener.onReconnect')
const newCardTree = this.state.cardTree.mutableCopy()
await newCardTree.sync()
this.setState({cardTree: newCardTree})
},
)
}
componentWillUnmount() {
this.cardListener?.close()
this.cardListener = undefined
}
render() {
const {cardTree} = this.state
const menu = (
<Menu position='left'>
<Menu.Text
@ -28,10 +84,22 @@ class CardDialog extends React.Component<Props> {
icon={<DeleteIcon/>}
name='Delete'
onClick={async () => {
await mutator.deleteBlock(this.props.card, 'delete card')
const card = this.state.cardTree?.card
if (!card) {
Utils.assertFailure()
return
}
await mutator.deleteBlock(card, 'delete card')
this.props.onClose()
}}
/>
{(cardTree && !cardTree.card.isTemplate) &&
<Menu.Text
id='makeTemplate'
name='New template from card'
onClick={this.makeTemplate}
/>
}
</Menu>
)
return (
@ -39,7 +107,7 @@ class CardDialog extends React.Component<Props> {
onClose={this.props.onClose}
toolsMenu={menu}
>
{(this.props.card.isTemplate) &&
{(cardTree?.card.isTemplate) &&
<div className='banner'>
<FormattedMessage
id='CardDialog.editing-template'
@ -47,13 +115,41 @@ class CardDialog extends React.Component<Props> {
/>
</div>
}
<CardDetail
boardTree={this.props.boardTree}
cardId={this.props.card.id}
/>
{this.state.cardTree &&
<CardDetail
boardTree={this.props.boardTree}
cardTree={this.state.cardTree}
/>
}
</Dialog>
)
}
private makeTemplate = async () => {
const {cardTree} = this.state
if (!cardTree) {
Utils.assertFailure('this.state.cardTree')
return
}
const newCardTree = cardTree.templateCopy()
newCardTree.card.isTemplate = true
newCardTree.card.title = 'New Template'
Utils.log(`Created new template: ${newCardTree.card.id}`)
const blocksToInsert = [newCardTree.card, ...newCardTree.contents]
await mutator.insertBlocks(
blocksToInsert,
'create template from card',
async () => {
this.props.showCard(newCardTree.card.id)
},
async () => {
this.props.showCard(undefined)
},
)
}
}
export {CardDialog}

View file

@ -6,7 +6,7 @@ import {FormattedMessage} from 'react-intl'
import {Constants} from '../constants'
import {BlockIcons} from '../blockIcons'
import {IPropertyTemplate} from '../blocks/board'
import {Card, MutableCard} from '../blocks/card'
import {MutableCard} from '../blocks/card'
import {BoardTree} from '../viewModel/boardTree'
import mutator from '../mutator'
import {Utils} from '../utils'
@ -36,7 +36,7 @@ type Props = {
}
type State = {
shownCard?: Card
shownCardId?: string
}
class TableComponent extends React.Component<Props, State> {
@ -76,12 +76,14 @@ class TableComponent extends React.Component<Props, State> {
return (
<div className='TableComponent octo-app'>
{this.state.shownCard &&
{this.state.shownCardId &&
<RootPortal>
<CardDialog
key={this.state.shownCardId}
boardTree={boardTree}
card={this.state.shownCard}
onClose={() => this.setState({shownCard: undefined})}
cardId={this.state.shownCardId}
onClose={() => this.setState({shownCardId: undefined})}
showCard={(cardId) => this.setState({shownCardId: cardId})}
/>
</RootPortal>}
<div className='octo-frame'>
@ -99,7 +101,6 @@ class TableComponent extends React.Component<Props, State> {
addCardFromTemplate={this.addCardFromTemplate}
addCardTemplate={this.addCardTemplate}
editCardTemplate={this.editCardTemplate}
deleteCardTemplate={this.deleteCardTemplate}
/>
{/* Main content */}
@ -266,6 +267,9 @@ class TableComponent extends React.Component<Props, State> {
}
console.log('STILL WORKING')
}}
showCard={(cardId) => {
this.setState({shownCardId: cardId})
}}
/>)
this.cardIdToRowMap.set(card.id, tableRowRef)
@ -303,20 +307,22 @@ class TableComponent extends React.Component<Props, State> {
this.addCard(true)
}
private addCardFromTemplate = async (cardTemplate?: Card) => {
this.addCard(true, cardTemplate)
private addCardFromTemplate = async (cardTemplateId?: string) => {
this.addCard(true, cardTemplateId)
}
private addCard = async (show = false, cardTemplate?: Card) => {
private addCard = async (show = false, cardTemplateId?: string) => {
const {boardTree} = this.props
let card: MutableCard
let blocksToInsert: IBlock[]
if (cardTemplate) {
const templateCardTree = new MutableCardTree(cardTemplate.id)
if (cardTemplateId) {
const templateCardTree = new MutableCardTree(cardTemplateId)
await templateCardTree.sync()
const newCardTree = templateCardTree.duplicateFromTemplate()
const newCardTree = templateCardTree.templateCopy()
card = newCardTree.card
card.isTemplate = false
card.title = ''
blocksToInsert = [newCardTree.card, ...newCardTree.contents]
} else {
card = new MutableCard()
@ -330,7 +336,7 @@ class TableComponent extends React.Component<Props, State> {
'add card',
async () => {
if (show) {
this.setState({shownCard: card})
this.setState({shownCardId: card.id})
} else {
// Focus on this card's title inline on next render
this.cardIdToFocusOnRender = card.id
@ -350,17 +356,13 @@ class TableComponent extends React.Component<Props, State> {
cardTemplate,
'add card',
async () => {
this.setState({shownCard: cardTemplate})
this.setState({shownCardId: cardTemplate.id})
},
)
}
private editCardTemplate = (cardTemplate: Card) => {
this.setState({shownCard: cardTemplate})
}
private deleteCardTemplate = (cardTemplate: Card) => {
mutator.deleteBlock(cardTemplate, 'delete card template')
private editCardTemplate = (cardTemplateId: string) => {
this.setState({shownCardId: cardTemplateId})
}
private async onDropToColumn(template: IPropertyTemplate) {

View file

@ -12,8 +12,6 @@ import Editable from '../widgets/editable'
import Button from '../widgets/buttons/button'
import PropertyValueElement from './propertyValueElement'
import {CardDialog} from './cardDialog'
import RootPortal from './rootPortal'
import './tableRow.scss'
@ -22,10 +20,10 @@ type Props = {
card: Card
focusOnMount: boolean
onSaveWithEnter: () => void
showCard: (cardId: string) => void
}
type State = {
showCard: boolean
title: string
}
@ -34,7 +32,6 @@ class TableRow extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
showCard: false,
title: props.card.title,
}
}
@ -84,21 +81,13 @@ class TableRow extends React.Component<Props, State> {
</div>
<div className='open-button'>
<Button onClick={() => this.setState({showCard: true})}>
<Button onClick={() => this.props.showCard(this.props.card.id)}>
<FormattedMessage
id='TableRow.open'
defaultMessage='Open'
/>
</Button>
</div>
{this.state.showCard &&
<RootPortal>
<CardDialog
boardTree={boardTree}
card={card}
onClose={() => this.setState({showCard: false})}
/>
</RootPortal>}
</div>
{/* Columns, one per property */}

View file

@ -6,7 +6,7 @@ import {injectIntl, IntlShape, FormattedMessage} from 'react-intl'
import {Archiver} from '../archiver'
import {ISortOption, MutableBoardView} from '../blocks/boardView'
import {BlockIcons} from '../blockIcons'
import {Card, MutableCard} from '../blocks/card'
import {MutableCard} from '../blocks/card'
import {IPropertyTemplate} from '../blocks/board'
import {BoardTree} from '../viewModel/boardTree'
import ViewMenu from '../components/viewMenu'
@ -38,10 +38,9 @@ type Props = {
showView: (id: string) => void
setSearchText: (text: string) => void
addCard: () => void
addCardFromTemplate: (cardTemplate?: Card) => void
addCardFromTemplate: (cardTemplateId?: string) => void
addCardTemplate: () => void
editCardTemplate: (cardTemplate: Card) => void
deleteCardTemplate: (cardTemplate: Card) => void
editCardTemplate: (cardTemplateId: string) => void
withGroupBy?: boolean
intl: IntlShape
}
@ -406,7 +405,7 @@ class ViewHeader extends React.Component<Props, State> {
id={cardTemplate.id}
name={cardTemplate.title || intl.formatMessage({id: 'ViewHeader.untitled', defaultMessage: 'Untitled'})}
onClick={() => {
this.props.addCardFromTemplate(cardTemplate)
this.props.addCardFromTemplate(cardTemplate.id)
}}
rightIcon={
<MenuWrapper stopPropagationOnToggle={true}>
@ -416,15 +415,15 @@ class ViewHeader extends React.Component<Props, State> {
id='edit'
name={intl.formatMessage({id: 'ViewHeader.edit-template', defaultMessage: 'Edit'})}
onClick={() => {
this.props.editCardTemplate(cardTemplate)
this.props.editCardTemplate(cardTemplate.id)
}}
/>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'ViewHeader.delete-template', defaultMessage: 'Delete'})}
onClick={() => {
this.props.deleteCardTemplate(cardTemplate)
onClick={async () => {
await mutator.deleteBlock(cardTemplate, 'delete card template')
}}
/>
</Menu>

View file

@ -13,6 +13,7 @@ interface CardTree {
readonly contents: readonly IOrderedBlock[]
mutableCopy(): MutableCardTree
templateCopy(): MutableCardTree
}
class MutableCardTree implements CardTree {
@ -57,8 +58,9 @@ class MutableCardTree implements CardTree {
return cardTree
}
duplicateFromTemplate(): MutableCardTree {
const card = this.card.newCardFromTemplate()
templateCopy(): MutableCardTree {
const card = this.card.duplicate()
const contents: IOrderedBlock[] = this.contents.map((content) => {
const copy = MutableBlock.duplicate(content)
copy.parentId = card.id

View file

@ -33,6 +33,7 @@
flex-direction: row;
align-items: center;
white-space: nowrap;
font-weight: 400;
padding: 2px 10px;
cursor: pointer;