mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-24 13:43:12 +02:00
Card templates
This commit is contained in:
parent
5b07bee7ec
commit
289f8f9d30
@ -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}
|
||||
|
@ -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) {
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user