1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-24 13:43:12 +02:00

Card templates

This commit is contained in:
Chen-I Lim 2020-11-10 11:23:08 -08:00
parent 5b07bee7ec
commit 289f8f9d30
9 changed files with 241 additions and 27 deletions

View File

@ -1,12 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Utils} from '../utils'
import {IBlock} from '../blocks/block'
import {MutableBlock} from './block'
interface Card extends IBlock {
readonly icon: string
readonly isTemplate: boolean
readonly properties: Readonly<Record<string, string>>
newCardFromTemplate(): MutableCard
}
class MutableCard extends MutableBlock {
@ -17,6 +20,13 @@ class MutableCard extends MutableBlock {
this.fields.icon = value
}
get isTemplate(): boolean {
return this.fields.isTemplate as boolean
}
set isTemplate(value: boolean) {
this.fields.isTemplate = value
}
get properties(): Record<string, string> {
return this.fields.properties as Record<string, string>
}
@ -30,6 +40,14 @@ class MutableCard extends MutableBlock {
this.properties = {...(block.fields?.properties || {})}
}
newCardFromTemplate(): MutableCard {
const card = new MutableCard(this)
card.id = Utils.createGuid()
card.isTemplate = false
card.title = ''
return card
}
}
export {MutableCard, Card}

View File

@ -6,8 +6,10 @@ import {injectIntl, IntlShape, FormattedMessage} from 'react-intl'
import {BlockIcons} from '../blockIcons'
import {IPropertyOption, IPropertyTemplate} from '../blocks/board'
import {IBlock} from '../blocks/block'
import {Card, MutableCard} from '../blocks/card'
import {BoardTree, BoardTreeGroup} from '../viewModel/boardTree'
import {MutableCardTree} from '../viewModel/cardTree'
import {CardFilter} from '../cardFilter'
import {Constants} from '../constants'
import mutator from '../mutator'
@ -150,6 +152,10 @@ class BoardComponent extends React.Component<Props, State> {
showView={showView}
setSearchText={this.props.setSearchText}
addCard={() => this.addCard()}
addCardFromTemplate={this.addCardFromTemplate}
addCardTemplate={() => this.addCardTemplate()}
editCardTemplate={this.editCardTemplate}
deleteCardTemplate={this.deleteCardTemplate}
withGroupBy={true}
/>
<div
@ -473,28 +479,80 @@ class BoardComponent extends React.Component<Props, State> {
}
}
private async addCard(groupByOptionId?: string): Promise<void> {
private addCardFromTemplate = async (cardTemplate?: Card) => {
this.addCard(undefined, cardTemplate)
}
private async addCard(groupByOptionId?: string, cardTemplate?: Card): Promise<void> {
const {boardTree} = this.props
const {activeView, board} = boardTree
const card = new MutableCard()
let card: MutableCard
let blocksToInsert: IBlock[]
if (cardTemplate) {
const templateCardTree = new MutableCardTree(cardTemplate.id)
await templateCardTree.sync()
const newCardTree = templateCardTree.duplicateFromTemplate()
card = newCardTree.card
blocksToInsert = [newCardTree.card, ...newCardTree.contents]
} else {
card = new MutableCard()
blocksToInsert = [card]
}
card.parentId = boardTree.board.id
card.properties = CardFilter.propertiesThatMeetFilterGroup(activeView.filter, board.cardProperties)
card.icon = BlockIcons.shared.randomIcon()
const propertiesThatMeetFilters = CardFilter.propertiesThatMeetFilterGroup(activeView.filter, board.cardProperties)
if (boardTree.groupByProperty) {
if (groupByOptionId) {
card.properties[boardTree.groupByProperty.id] = groupByOptionId
propertiesThatMeetFilters[boardTree.groupByProperty.id] = groupByOptionId
} else {
delete card.properties[boardTree.groupByProperty.id]
delete propertiesThatMeetFilters[boardTree.groupByProperty.id]
}
}
await mutator.insertBlock(card, 'add card', async () => {
this.setState({shownCard: card})
card.properties = {...card.properties, ...propertiesThatMeetFilters}
card.icon = BlockIcons.shared.randomIcon()
await mutator.insertBlocks(
blocksToInsert,
'add card',
async () => {
this.setState({shownCard: card})
},
async () => {
this.setState({shownCard: undefined})
},
)
}
private async addCardTemplate(groupByOptionId?: string): Promise<void> {
const {boardTree} = this.props
const {activeView, board} = boardTree
const cardTemplate = new MutableCard()
cardTemplate.isTemplate = true
cardTemplate.parentId = boardTree.board.id
cardTemplate.properties = CardFilter.propertiesThatMeetFilterGroup(activeView.filter, board.cardProperties)
if (boardTree.groupByProperty) {
if (groupByOptionId) {
cardTemplate.properties[boardTree.groupByProperty.id] = groupByOptionId
} else {
delete cardTemplate.properties[boardTree.groupByProperty.id]
}
}
await mutator.insertBlock(cardTemplate, 'add card template', async () => {
this.setState({shownCard: cardTemplate})
}, async () => {
this.setState({shownCard: undefined})
})
}
private editCardTemplate = (cardTemplate: Card) => {
this.setState({shownCard: cardTemplate})
}
private deleteCardTemplate = (cardTemplate: Card) => {
mutator.deleteBlock(cardTemplate, 'delete card template')
}
private async propertyNameChanged(option: IPropertyOption, text: string): Promise<void> {
const {boardTree} = this.props
@ -554,7 +612,6 @@ class BoardComponent extends React.Component<Props, State> {
const {draggedCards, draggedHeaderOption} = this
const optionId = option ? option.id : undefined
Utils.assertValue(mutator)
Utils.assertValue(boardTree)
if (draggedCards.length > 0) {

View File

@ -2,6 +2,8 @@
// See LICENSE.txt for license information.
import React from 'react'
import {FormattedMessage} from 'react-intl'
import {Card} from '../blocks/card'
import {BoardTree} from '../viewModel/boardTree'
import mutator from '../mutator'
@ -37,6 +39,14 @@ class CardDialog extends React.Component<Props> {
onClose={this.props.onClose}
toolsMenu={menu}
>
{(this.props.card.isTemplate) &&
<div className='banner'>
<FormattedMessage
id='CardDialog.editing-template'
defaultMessage="You're editing a template"
/>
</div>
}
<CardDetail
boardTree={this.props.boardTree}
cardId={this.props.card.id}

View File

@ -24,6 +24,11 @@
overflow-x: hidden;
overflow-y: auto;
> .banner {
background-color: rgba(230, 220, 192, 0.9);
text-align: center;
padding: 10px;
}
> .toolbar {
display: flex;
flex-direction: row;

View File

@ -26,6 +26,8 @@ import './tableComponent.scss'
import {HorizontalGrip} from './horizontalGrip'
import {MutableBoardView} from '../blocks/boardView'
import {IBlock} from '../blocks/block'
import {MutableCardTree} from '../viewModel/cardTree'
type Props = {
boardTree?: BoardTree
@ -93,7 +95,11 @@ class TableComponent extends React.Component<Props, State> {
boardTree={boardTree}
showView={showView}
setSearchText={this.props.setSearchText}
addCard={this.addCard}
addCard={this.addCardAndShow}
addCardFromTemplate={this.addCardFromTemplate}
addCardTemplate={this.addCardTemplate}
editCardTemplate={this.editCardTemplate}
deleteCardTemplate={this.deleteCardTemplate}
/>
{/* Main content */}
@ -293,14 +299,34 @@ class TableComponent extends React.Component<Props, State> {
return Math.max(Constants.minColumnWidth, this.props.boardTree?.activeView?.columnWidths[templateId] || 0)
}
private addCard = async (show = false) => {
private addCardAndShow = () => {
this.addCard(true)
}
private addCardFromTemplate = async (cardTemplate?: Card) => {
this.addCard(true, cardTemplate)
}
private addCard = async (show = false, cardTemplate?: Card) => {
const {boardTree} = this.props
const card = new MutableCard()
let card: MutableCard
let blocksToInsert: IBlock[]
if (cardTemplate) {
const templateCardTree = new MutableCardTree(cardTemplate.id)
await templateCardTree.sync()
const newCardTree = templateCardTree.duplicateFromTemplate()
card = newCardTree.card
blocksToInsert = [newCardTree.card, ...newCardTree.contents]
} else {
card = new MutableCard()
blocksToInsert = [card]
}
card.parentId = boardTree.board.id
card.icon = BlockIcons.shared.randomIcon()
await mutator.insertBlock(
card,
await mutator.insertBlocks(
blocksToInsert,
'add card',
async () => {
if (show) {
@ -313,6 +339,30 @@ class TableComponent extends React.Component<Props, State> {
)
}
private addCardTemplate = async () => {
const {boardTree} = this.props
const cardTemplate = new MutableCard()
cardTemplate.isTemplate = true
cardTemplate.parentId = boardTree.board.id
cardTemplate.icon = BlockIcons.shared.randomIcon()
await mutator.insertBlock(
cardTemplate,
'add card',
async () => {
this.setState({shownCard: cardTemplate})
},
)
}
private editCardTemplate = (cardTemplate: Card) => {
this.setState({shownCard: cardTemplate})
}
private deleteCardTemplate = (cardTemplate: Card) => {
mutator.deleteBlock(cardTemplate, 'delete card template')
}
private async onDropToColumn(template: IPropertyTemplate) {
const {draggedHeaderTemplate} = this
if (!draggedHeaderTemplate) {

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 {MutableCard} from '../blocks/card'
import {Card, MutableCard} from '../blocks/card'
import {IPropertyTemplate} from '../blocks/board'
import {BoardTree} from '../viewModel/boardTree'
import ViewMenu from '../components/viewMenu'
@ -29,15 +29,19 @@ import {Editable} from './editable'
import FilterComponent from './filterComponent'
import './viewHeader.scss'
import {sendFlashMessage} from './flashMessages'
import {Constants} from '../constants'
import DeleteIcon from '../widgets/icons/delete'
type Props = {
boardTree?: BoardTree
showView: (id: string) => void
setSearchText: (text: string) => void
addCard: (show: boolean) => void
addCard: () => void
addCardFromTemplate: (cardTemplate?: Card) => void
addCardTemplate: () => void
editCardTemplate: (cardTemplate: Card) => void
deleteCardTemplate: (cardTemplate: Card) => void
withGroupBy?: boolean
intl: IntlShape
}
@ -371,9 +375,10 @@ class ViewHeader extends React.Component<Props, State> {
/>
</Menu>
</MenuWrapper>
<ButtonWithMenu
onClick={() => {
this.props.addCard(true)
this.props.addCard()
}}
text={(
<FormattedMessage
@ -391,10 +396,56 @@ class ViewHeader extends React.Component<Props, State> {
/>
</b>
</Menu.Label>
<Menu.Separator/>
{boardTree.cardTemplates.map((cardTemplate) => {
return (
<Menu.Text
key={cardTemplate.id}
id={cardTemplate.id}
name={cardTemplate.title || intl.formatMessage({id: 'ViewHeader.untitled', defaultMessage: 'Untitled'})}
onClick={() => {
this.props.addCardFromTemplate(cardTemplate)
}}
rightIcon={
<MenuWrapper stopPropagationOnToggle={true}>
<IconButton icon={<OptionsIcon/>}/>
<Menu position='left'>
<Menu.Text
id='edit'
name={intl.formatMessage({id: 'ViewHeader.edit-template', defaultMessage: 'Edit'})}
onClick={() => {
this.props.editCardTemplate(cardTemplate)
}}
/>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'ViewHeader.delete-template', defaultMessage: 'Delete'})}
onClick={() => {
this.props.deleteCardTemplate(cardTemplate)
}}
/>
</Menu>
</MenuWrapper>
}
/>
)
})}
<Menu.Text
id='example-template'
name={intl.formatMessage({id: 'ViewHeader.sample-templte', defaultMessage: 'Sample template'})}
onClick={() => sendFlashMessage({content: 'Not implemented yet', severity: 'low'})}
id='empty-template'
name={intl.formatMessage({id: 'ViewHeader.empty-card', defaultMessage: 'Empty card'})}
onClick={() => {
this.props.addCard()
}}
/>
<Menu.Text
id='add-template'
name={intl.formatMessage({id: 'ViewHeader.add-template', defaultMessage: '+ New template'})}
onClick={() => this.props.addCardTemplate()}
/>
</Menu>
</ButtonWithMenu>

View File

@ -19,6 +19,7 @@ interface BoardTree {
readonly board: Board
readonly views: readonly BoardView[]
readonly cards: readonly Card[]
readonly cardTemplates: readonly Card[]
readonly allCards: readonly Card[]
readonly visibleGroups: readonly Group[]
readonly hiddenGroups: readonly Group[]
@ -37,6 +38,7 @@ class MutableBoardTree implements BoardTree {
board!: MutableBoard
views: MutableBoardView[] = []
cards: MutableCard[] = []
cardTemplates: MutableCard[] = []
visibleGroups: Group[] = []
hiddenGroups: Group[] = []
@ -47,7 +49,7 @@ class MutableBoardTree implements BoardTree {
private searchText?: string
allCards: MutableCard[] = []
get allBlocks(): IBlock[] {
return [this.board, ...this.views, ...this.allCards]
return [this.board, ...this.views, ...this.allCards, ...this.cardTemplates]
}
constructor(private boardId: string) {
@ -71,7 +73,8 @@ class MutableBoardTree implements BoardTree {
private rebuild(blocks: IMutableBlock[]) {
this.board = blocks.find((block) => block.type === 'board') as MutableBoard
this.views = blocks.filter((block) => block.type === 'view') as MutableBoardView[]
this.allCards = blocks.filter((block) => block.type === 'card') as MutableCard[]
this.allCards = blocks.filter((block) => block.type === 'card' && !(block as Card).isTemplate) as MutableCard[]
this.cardTemplates = blocks.filter((block) => block.type === 'card' && (block as Card).isTemplate) as MutableCard[]
this.cards = []
this.ensureMinimumSchema()

View File

@ -1,10 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Card} from '../blocks/card'
import {Card, MutableCard} from '../blocks/card'
import {IOrderedBlock} from '../blocks/orderedBlock'
import octoClient from '../octoClient'
import {IBlock} from '../blocks/block'
import {IBlock, MutableBlock} from '../blocks/block'
import {OctoUtils} from '../octoUtils'
import {Utils} from '../utils'
interface CardTree {
readonly card: Card
@ -15,7 +16,7 @@ interface CardTree {
}
class MutableCardTree implements CardTree {
card: Card
card: MutableCard
comments: IBlock[] = []
contents: IOrderedBlock[] = []
@ -40,7 +41,7 @@ class MutableCardTree implements CardTree {
}
private rebuild(blocks: IBlock[]) {
this.card = blocks.find((o) => o.id === this.cardId) as Card
this.card = blocks.find((o) => o.id === this.cardId) as MutableCard
this.comments = blocks.
filter((block) => block.type === 'comment').
@ -55,6 +56,19 @@ class MutableCardTree implements CardTree {
cardTree.incrementalUpdate(this.rawBlocks)
return cardTree
}
duplicateFromTemplate(): MutableCardTree {
const card = this.card.newCardFromTemplate()
const contents: IOrderedBlock[] = this.contents.map((content) => {
const copy = MutableBlock.duplicate(content)
copy.parentId = card.id
return copy as IOrderedBlock
})
const cardTree = new MutableCardTree(card.id)
cardTree.incrementalUpdate([card, ...contents])
return cardTree
}
}
export {MutableCardTree, CardTree}

View File

@ -19,6 +19,8 @@
display: flex;
flex-direction: column;
flex-grow: 1;
list-style: none;
padding: 0;
margin: 0;
@ -36,6 +38,10 @@
cursor: pointer;
touch-action: none;
* {
display: flex;
}
&:hover {
background: rgba(90, 90, 90, 0.1);
}