From 57d7eb35bd130e6ac146cb608754307483df669b Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Wed, 11 Nov 2020 09:21:16 -0800 Subject: [PATCH] Create template from card --- webapp/src/blocks/card.ts | 6 +- webapp/src/components/boardComponent.tsx | 43 ++++----- webapp/src/components/cardDetail.tsx | 63 ++---------- webapp/src/components/cardDialog.tsx | 116 +++++++++++++++++++++-- webapp/src/components/tableComponent.tsx | 42 ++++---- webapp/src/components/tableRow.tsx | 15 +-- webapp/src/components/viewHeader.tsx | 15 ++- webapp/src/viewModel/cardTree.ts | 6 +- webapp/src/widgets/menu/menu.scss | 1 + 9 files changed, 174 insertions(+), 133 deletions(-) diff --git a/webapp/src/blocks/card.ts b/webapp/src/blocks/card.ts index bde93fd6a..495e9b091 100644 --- a/webapp/src/blocks/card.ts +++ b/webapp/src/blocks/card.ts @@ -9,7 +9,7 @@ interface Card extends IBlock { readonly icon: string readonly isTemplate: boolean readonly properties: Readonly> - 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 } } diff --git a/webapp/src/components/boardComponent.tsx b/webapp/src/components/boardComponent.tsx index 38b738724..d7b8f7bb6 100644 --- a/webapp/src/components/boardComponent.tsx +++ b/webapp/src/components/boardComponent.tsx @@ -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 { this.backgroundClicked(e) }} > - {this.state.shownCard && + {this.state.shownCardId && this.setState({shownCard: undefined})} + cardId={this.state.shownCardId} + onClose={() => this.setState({shownCardId: undefined})} + showCard={(cardId) => this.setState({shownCardId: cardId})} /> } @@ -155,7 +157,6 @@ class BoardComponent extends React.Component { addCardFromTemplate={this.addCardFromTemplate} addCardTemplate={() => this.addCardTemplate()} editCardTemplate={this.editCardTemplate} - deleteCardTemplate={this.deleteCardTemplate} withGroupBy={true} />
{ } } - 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 { + private async addCard(groupByOptionId?: string, cardTemplateId?: string): Promise { 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 { 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 { } } 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 { @@ -586,7 +585,7 @@ class BoardComponent extends React.Component { this.setState({selectedCardIds}) } } else { - this.setState({selectedCardIds: [], shownCard: card}) + this.setState({selectedCardIds: [], shownCardId: card.id}) } e.stopPropagation() diff --git a/webapp/src/components/cardDetail.tsx b/webapp/src/components/cardDetail.tsx index f62c84cfd..48b3d68d2 100644 --- a/webapp/src/components/cardDetail.tsx +++ b/webapp/src/components/cardDetail.tsx @@ -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 { private titleRef = React.createRef() - private cardListener?: OctoListener shouldComponentUpdate() { return true @@ -49,54 +46,12 @@ class CardDetail extends React.Component { 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 { 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 { 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 { 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 { 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', )} /> diff --git a/webapp/src/components/cardDialog.tsx b/webapp/src/components/cardDialog.tsx index 0727c5ccd..27e95bd0b 100644 --- a/webapp/src/components/cardDialog.tsx +++ b/webapp/src/components/cardDialog.tsx @@ -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 { +type State = { + cardTree?: CardTree +} + +class CardDialog extends React.Component { + 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 = ( { icon={} 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) && + + } ) return ( @@ -39,7 +107,7 @@ class CardDialog extends React.Component { onClose={this.props.onClose} toolsMenu={menu} > - {(this.props.card.isTemplate) && + {(cardTree?.card.isTemplate) &&
{ />
} - + {this.state.cardTree && + + } ) } + + 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} diff --git a/webapp/src/components/tableComponent.tsx b/webapp/src/components/tableComponent.tsx index 70b1a6d78..e0eb5cdd7 100644 --- a/webapp/src/components/tableComponent.tsx +++ b/webapp/src/components/tableComponent.tsx @@ -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 { @@ -76,12 +76,14 @@ class TableComponent extends React.Component { return (
- {this.state.shownCard && + {this.state.shownCardId && this.setState({shownCard: undefined})} + cardId={this.state.shownCardId} + onClose={() => this.setState({shownCardId: undefined})} + showCard={(cardId) => this.setState({shownCardId: cardId})} /> }
@@ -99,7 +101,6 @@ class TableComponent extends React.Component { addCardFromTemplate={this.addCardFromTemplate} addCardTemplate={this.addCardTemplate} editCardTemplate={this.editCardTemplate} - deleteCardTemplate={this.deleteCardTemplate} /> {/* Main content */} @@ -266,6 +267,9 @@ class TableComponent extends React.Component { } 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 { 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 { '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 { 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) { diff --git a/webapp/src/components/tableRow.tsx b/webapp/src/components/tableRow.tsx index 26a8253f0..41d7c4aeb 100644 --- a/webapp/src/components/tableRow.tsx +++ b/webapp/src/components/tableRow.tsx @@ -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 { constructor(props: Props) { super(props) this.state = { - showCard: false, title: props.card.title, } } @@ -84,21 +81,13 @@ class TableRow extends React.Component {
-
- {this.state.showCard && - - this.setState({showCard: false})} - /> - }
{/* Columns, one per property */} diff --git a/webapp/src/components/viewHeader.tsx b/webapp/src/components/viewHeader.tsx index 6ae598756..35240dc76 100644 --- a/webapp/src/components/viewHeader.tsx +++ b/webapp/src/components/viewHeader.tsx @@ -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 { id={cardTemplate.id} name={cardTemplate.title || intl.formatMessage({id: 'ViewHeader.untitled', defaultMessage: 'Untitled'})} onClick={() => { - this.props.addCardFromTemplate(cardTemplate) + this.props.addCardFromTemplate(cardTemplate.id) }} rightIcon={ @@ -416,15 +415,15 @@ class ViewHeader extends React.Component { id='edit' name={intl.formatMessage({id: 'ViewHeader.edit-template', defaultMessage: 'Edit'})} onClick={() => { - this.props.editCardTemplate(cardTemplate) + this.props.editCardTemplate(cardTemplate.id) }} /> } 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') }} /> diff --git a/webapp/src/viewModel/cardTree.ts b/webapp/src/viewModel/cardTree.ts index 32c77133e..1ca71d959 100644 --- a/webapp/src/viewModel/cardTree.ts +++ b/webapp/src/viewModel/cardTree.ts @@ -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 diff --git a/webapp/src/widgets/menu/menu.scss b/webapp/src/widgets/menu/menu.scss index cd6f8a090..24e4aca46 100644 --- a/webapp/src/widgets/menu/menu.scss +++ b/webapp/src/widgets/menu/menu.scss @@ -33,6 +33,7 @@ flex-direction: row; align-items: center; + white-space: nowrap; font-weight: 400; padding: 2px 10px; cursor: pointer;