mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-02 14:47:55 +02:00
Board templates
This commit is contained in:
parent
a704dde733
commit
02d26a800a
@ -1,5 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
import {Utils} from '../utils'
|
||||||
|
|
||||||
import {IBlock} from '../blocks/block'
|
import {IBlock} from '../blocks/block'
|
||||||
|
|
||||||
import {MutableBlock} from './block'
|
import {MutableBlock} from './block'
|
||||||
@ -35,7 +37,9 @@ interface IMutablePropertyTemplate extends IPropertyTemplate {
|
|||||||
|
|
||||||
interface Board extends IBlock {
|
interface Board extends IBlock {
|
||||||
readonly icon: string
|
readonly icon: string
|
||||||
|
readonly isTemplate: boolean
|
||||||
readonly cardProperties: readonly IPropertyTemplate[]
|
readonly cardProperties: readonly IPropertyTemplate[]
|
||||||
|
duplicate(): MutableBoard
|
||||||
}
|
}
|
||||||
|
|
||||||
class MutableBoard extends MutableBlock {
|
class MutableBoard extends MutableBlock {
|
||||||
@ -46,6 +50,13 @@ class MutableBoard extends MutableBlock {
|
|||||||
this.fields.icon = value
|
this.fields.icon = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isTemplate(): boolean {
|
||||||
|
return Boolean(this.fields.isTemplate)
|
||||||
|
}
|
||||||
|
set isTemplate(value: boolean) {
|
||||||
|
this.fields.isTemplate = value
|
||||||
|
}
|
||||||
|
|
||||||
get cardProperties(): IMutablePropertyTemplate[] {
|
get cardProperties(): IMutablePropertyTemplate[] {
|
||||||
return this.fields.cardProperties as IPropertyTemplate[]
|
return this.fields.cardProperties as IPropertyTemplate[]
|
||||||
}
|
}
|
||||||
@ -72,6 +83,12 @@ class MutableBoard extends MutableBlock {
|
|||||||
this.cardProperties = []
|
this.cardProperties = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duplicate(): MutableBoard {
|
||||||
|
const card = new MutableBoard(this)
|
||||||
|
card.id = Utils.createGuid()
|
||||||
|
return card
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {Board, MutableBoard, PropertyType, IPropertyOption, IPropertyTemplate}
|
export {Board, MutableBoard, PropertyType, IPropertyOption, IPropertyTemplate}
|
||||||
|
@ -21,7 +21,7 @@ class MutableCard extends MutableBlock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isTemplate(): boolean {
|
get isTemplate(): boolean {
|
||||||
return this.fields.isTemplate as boolean
|
return Boolean(this.fields.isTemplate)
|
||||||
}
|
}
|
||||||
set isTemplate(value: boolean) {
|
set isTemplate(value: boolean) {
|
||||||
this.fields.isTemplate = value
|
this.fields.isTemplate = value
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 3px 20px;
|
padding: 3px 20px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
.IconButton {
|
>.IconButton {
|
||||||
background-color: var(--sidebar-bg);
|
background-color: var(--sidebar-bg);
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(var(--sidebar-fg), 0.1);
|
background-color: rgba(var(--sidebar-fg), 0.1);
|
||||||
@ -87,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.IconButton {
|
>.IconButton {
|
||||||
background-color: var(--sidebar-bg);
|
background-color: var(--sidebar-bg);
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(var(--sidebar-fg), 0.1);
|
background-color: rgba(var(--sidebar-fg), 0.1);
|
||||||
@ -127,6 +127,10 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Menu .OptionsIcon {
|
||||||
|
fill: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.HideSidebarIcon {
|
.HideSidebarIcon {
|
||||||
stroke: rgba(var(--sidebar-fg), 0.5);
|
stroke: rgba(var(--sidebar-fg), 0.5);
|
||||||
stroke-width: 6px;
|
stroke-width: 6px;
|
||||||
|
@ -4,21 +4,23 @@ import React from 'react'
|
|||||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
||||||
|
|
||||||
import {Archiver} from '../archiver'
|
import {Archiver} from '../archiver'
|
||||||
|
import {IBlock} from '../blocks/block'
|
||||||
import {Board, MutableBoard} from '../blocks/board'
|
import {Board, MutableBoard} from '../blocks/board'
|
||||||
import {BoardView, MutableBoardView} from '../blocks/boardView'
|
import {BoardView, MutableBoardView} from '../blocks/boardView'
|
||||||
import mutator from '../mutator'
|
import mutator from '../mutator'
|
||||||
import {darkTheme, lightTheme, mattermostTheme, setTheme} from '../theme'
|
import {darkTheme, lightTheme, mattermostTheme, setTheme} from '../theme'
|
||||||
|
import {MutableBoardTree} from '../viewModel/boardTree'
|
||||||
import {WorkspaceTree} from '../viewModel/workspaceTree'
|
import {WorkspaceTree} from '../viewModel/workspaceTree'
|
||||||
import Button from '../widgets/buttons/button'
|
import Button from '../widgets/buttons/button'
|
||||||
import IconButton from '../widgets/buttons/iconButton'
|
import IconButton from '../widgets/buttons/iconButton'
|
||||||
import DeleteIcon from '../widgets/icons/delete'
|
import DeleteIcon from '../widgets/icons/delete'
|
||||||
|
import DisclosureTriangle from '../widgets/icons/disclosureTriangle'
|
||||||
import DotIcon from '../widgets/icons/dot'
|
import DotIcon from '../widgets/icons/dot'
|
||||||
import DuplicateIcon from '../widgets/icons/duplicate'
|
import DuplicateIcon from '../widgets/icons/duplicate'
|
||||||
import HamburgerIcon from '../widgets/icons/hamburger'
|
import HamburgerIcon from '../widgets/icons/hamburger'
|
||||||
import HideSidebarIcon from '../widgets/icons/hideSidebar'
|
import HideSidebarIcon from '../widgets/icons/hideSidebar'
|
||||||
import OptionsIcon from '../widgets/icons/options'
|
import OptionsIcon from '../widgets/icons/options'
|
||||||
import ShowSidebarIcon from '../widgets/icons/showSidebar'
|
import ShowSidebarIcon from '../widgets/icons/showSidebar'
|
||||||
import DisclosureTriangle from '../widgets/icons/disclosureTriangle'
|
|
||||||
import Menu from '../widgets/menu'
|
import Menu from '../widgets/menu'
|
||||||
import MenuWrapper from '../widgets/menuWrapper'
|
import MenuWrapper from '../widgets/menuWrapper'
|
||||||
import './sidebar.scss'
|
import './sidebar.scss'
|
||||||
@ -185,14 +187,77 @@ class Sidebar extends React.Component<Props, State> {
|
|||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<Button
|
<MenuWrapper>
|
||||||
onClick={this.addBoardClicked}
|
<Button>
|
||||||
>
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='Sidebar.add-board'
|
||||||
id='Sidebar.add-board'
|
defaultMessage='+ Add Board'
|
||||||
defaultMessage='+ Add Board'
|
/>
|
||||||
/>
|
</Button>
|
||||||
</Button>
|
<Menu position='top'>
|
||||||
|
<Menu.Label>
|
||||||
|
<b>
|
||||||
|
<FormattedMessage
|
||||||
|
id='Sidebar.select-a-template'
|
||||||
|
defaultMessage='Select a template'
|
||||||
|
/>
|
||||||
|
</b>
|
||||||
|
</Menu.Label>
|
||||||
|
|
||||||
|
<Menu.Separator/>
|
||||||
|
|
||||||
|
{workspaceTree.boardTemplates.map((boardTemplate) => {
|
||||||
|
let displayName = boardTemplate.title || intl.formatMessage({id: 'Sidebar.untitled', defaultMessage: 'Untitled'})
|
||||||
|
if (boardTemplate.icon) {
|
||||||
|
displayName = `${boardTemplate.icon} ${displayName}`
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Menu.Text
|
||||||
|
key={boardTemplate.id}
|
||||||
|
id={boardTemplate.id}
|
||||||
|
name={displayName}
|
||||||
|
onClick={() => {
|
||||||
|
this.addBoardClicked(boardTemplate.id)
|
||||||
|
}}
|
||||||
|
rightIcon={
|
||||||
|
<MenuWrapper stopPropagationOnToggle={true}>
|
||||||
|
<IconButton icon={<OptionsIcon/>}/>
|
||||||
|
<Menu position='left'>
|
||||||
|
<Menu.Text
|
||||||
|
id='edit'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})}
|
||||||
|
onClick={() => {
|
||||||
|
this.props.showBoard(boardTemplate.id)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Menu.Text
|
||||||
|
icon={<DeleteIcon/>}
|
||||||
|
id='delete'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.delete-template', defaultMessage: 'Delete'})}
|
||||||
|
onClick={async () => {
|
||||||
|
await mutator.deleteBlock(boardTemplate, 'delete board template')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</MenuWrapper>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Menu.Text
|
||||||
|
id='empty-template'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.empty-board', defaultMessage: 'Empty board'})}
|
||||||
|
onClick={this.addBoardClicked}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Menu.Text
|
||||||
|
id='add-template'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.add-template', defaultMessage: '+ New template'})}
|
||||||
|
onClick={this.addBoardTemplateClicked}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</MenuWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='octo-spacer'/>
|
<div className='octo-spacer'/>
|
||||||
@ -266,18 +331,34 @@ class Sidebar extends React.Component<Props, State> {
|
|||||||
this.props.showView(view.id, board.id)
|
this.props.showView(view.id, board.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private addBoardClicked = async () => {
|
private addBoardClicked = async (boardTemplateId?: string) => {
|
||||||
const {showBoard, intl} = this.props
|
const {showBoard, intl} = this.props
|
||||||
|
|
||||||
const oldBoardId = this.props.activeBoardId
|
const oldBoardId = this.props.activeBoardId
|
||||||
const board = new MutableBoard()
|
let board: MutableBoard
|
||||||
const view = new MutableBoardView()
|
const blocksToInsert: IBlock[] = []
|
||||||
view.viewType = 'board'
|
|
||||||
view.parentId = board.id
|
if (boardTemplateId) {
|
||||||
view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board View'})
|
const templateBoardTree = new MutableBoardTree(boardTemplateId)
|
||||||
|
await templateBoardTree.sync()
|
||||||
|
const newBoardTree = templateBoardTree.templateCopy()
|
||||||
|
board = newBoardTree.board
|
||||||
|
board.isTemplate = false
|
||||||
|
board.title = ''
|
||||||
|
blocksToInsert.push(...newBoardTree.allBlocks)
|
||||||
|
} else {
|
||||||
|
board = new MutableBoard()
|
||||||
|
blocksToInsert.push(board)
|
||||||
|
|
||||||
|
const view = new MutableBoardView()
|
||||||
|
view.viewType = 'board'
|
||||||
|
view.parentId = board.id
|
||||||
|
view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board View'})
|
||||||
|
blocksToInsert.push(view)
|
||||||
|
}
|
||||||
|
|
||||||
await mutator.insertBlocks(
|
await mutator.insertBlocks(
|
||||||
[board, view],
|
blocksToInsert,
|
||||||
'add board',
|
'add board',
|
||||||
async () => {
|
async () => {
|
||||||
showBoard(board.id)
|
showBoard(board.id)
|
||||||
@ -290,6 +371,24 @@ class Sidebar extends React.Component<Props, State> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addBoardTemplateClicked = async () => {
|
||||||
|
const {activeBoardId} = this.props
|
||||||
|
|
||||||
|
const boardTemplate = new MutableBoard()
|
||||||
|
boardTemplate.isTemplate = true
|
||||||
|
await mutator.insertBlock(
|
||||||
|
boardTemplate,
|
||||||
|
'add board template',
|
||||||
|
async () => {
|
||||||
|
this.props.showBoard(boardTemplate.id)
|
||||||
|
}, async () => {
|
||||||
|
if (activeBoardId) {
|
||||||
|
this.props.showBoard(activeBoardId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private hideClicked = () => {
|
private hideClicked = () => {
|
||||||
this.setState({isHidden: true})
|
this.setState({isHidden: true})
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
||||||
|
|
||||||
|
import {Utils} from '../utils'
|
||||||
|
|
||||||
import {Archiver} from '../archiver'
|
import {Archiver} from '../archiver'
|
||||||
import {BlockIcons} from '../blockIcons'
|
import {BlockIcons} from '../blockIcons'
|
||||||
import {IPropertyTemplate} from '../blocks/board'
|
import {IPropertyTemplate} from '../blocks/board'
|
||||||
@ -349,6 +351,11 @@ class ViewHeader extends React.Component<Props, State> {
|
|||||||
name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export Board Archive'})}
|
name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export Board Archive'})}
|
||||||
onClick={() => Archiver.exportBoardTree(boardTree)}
|
onClick={() => Archiver.exportBoardTree(boardTree)}
|
||||||
/>
|
/>
|
||||||
|
<Menu.Text
|
||||||
|
id='newTemplateFromBoard'
|
||||||
|
name={intl.formatMessage({id: 'ViewHeader.new-template-from-board', defaultMessage: 'New template from board'})}
|
||||||
|
onClick={this.newTemplateFromBoardClicked}
|
||||||
|
/>
|
||||||
|
|
||||||
<Menu.Separator/>
|
<Menu.Separator/>
|
||||||
|
|
||||||
@ -464,6 +471,22 @@ class ViewHeader extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private newTemplateFromBoardClicked = async () => {
|
||||||
|
const {boardTree} = this.props
|
||||||
|
|
||||||
|
const newBoardTree = boardTree.templateCopy()
|
||||||
|
newBoardTree.board.isTemplate = true
|
||||||
|
newBoardTree.board.title = 'New Board Template'
|
||||||
|
|
||||||
|
Utils.log(`Created new board template: ${newBoardTree.board.id}`)
|
||||||
|
|
||||||
|
const blocksToInsert = newBoardTree.allBlocks
|
||||||
|
await mutator.insertBlocks(
|
||||||
|
blocksToInsert,
|
||||||
|
'create template from board',
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(ViewHeader)
|
export default injectIntl(ViewHeader)
|
||||||
|
@ -2,5 +2,17 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
> .mainFrame {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> .banner {
|
||||||
|
background-color: rgba(230, 220, 192, 0.9);
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {FormattedMessage} from 'react-intl'
|
||||||
|
|
||||||
import {Utils} from '../utils'
|
import {Utils} from '../utils'
|
||||||
import {BoardTree} from '../viewModel/boardTree'
|
import {BoardTree} from '../viewModel/boardTree'
|
||||||
@ -34,7 +35,17 @@ class WorkspaceComponent extends React.PureComponent<Props> {
|
|||||||
activeBoardId={boardTree?.board.id}
|
activeBoardId={boardTree?.board.id}
|
||||||
setLanguage={setLanguage}
|
setLanguage={setLanguage}
|
||||||
/>
|
/>
|
||||||
{this.mainComponent()}
|
<div className='mainFrame'>
|
||||||
|
{(boardTree?.board.isTemplate) &&
|
||||||
|
<div className='banner'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='WorkspaceComponent.editing-board-template'
|
||||||
|
defaultMessage="You're editing a board template"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{this.mainComponent()}
|
||||||
|
</div>
|
||||||
</div>)
|
</div>)
|
||||||
|
|
||||||
return element
|
return element
|
||||||
|
@ -486,40 +486,36 @@ class Mutator {
|
|||||||
|
|
||||||
async duplicateCard(cardId: string, description = 'duplicate card', afterRedo?: (newBoardId: string) => Promise<void>, beforeUndo?: () => Promise<void>): Promise<[IBlock[], string]> {
|
async duplicateCard(cardId: string, description = 'duplicate card', afterRedo?: (newBoardId: string) => Promise<void>, beforeUndo?: () => Promise<void>): Promise<[IBlock[], string]> {
|
||||||
const blocks = await octoClient.getSubtree(cardId, 2)
|
const blocks = await octoClient.getSubtree(cardId, 2)
|
||||||
const [newBlocks1, idMap] = OctoUtils.duplicateBlockTree(blocks, cardId)
|
const [newBlocks1, newCard] = OctoUtils.duplicateBlockTree(blocks, cardId)
|
||||||
const newBlocks = newBlocks1.filter((o) => o.type !== 'comment')
|
const newBlocks = newBlocks1.filter((o) => o.type !== 'comment')
|
||||||
Utils.log(`duplicateCard: duplicating ${newBlocks.length} blocks`)
|
Utils.log(`duplicateCard: duplicating ${newBlocks.length} blocks`)
|
||||||
const newCardId = idMap[cardId]
|
|
||||||
const newCard = newBlocks.find((o) => o.id === newCardId)!
|
|
||||||
newCard.title = `Copy of ${newCard.title}`
|
newCard.title = `Copy of ${newCard.title}`
|
||||||
await this.insertBlocks(
|
await this.insertBlocks(
|
||||||
newBlocks,
|
newBlocks,
|
||||||
description,
|
description,
|
||||||
async () => {
|
async () => {
|
||||||
await afterRedo?.(newCardId)
|
await afterRedo?.(newCard.id)
|
||||||
},
|
},
|
||||||
beforeUndo,
|
beforeUndo,
|
||||||
)
|
)
|
||||||
return [newBlocks, newCardId]
|
return [newBlocks, newCard.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
async duplicateBoard(boardId: string, description = 'duplicate board', afterRedo?: (newBoardId: string) => Promise<void>, beforeUndo?: () => Promise<void>): Promise<[IBlock[], string]> {
|
async duplicateBoard(boardId: string, description = 'duplicate board', afterRedo?: (newBoardId: string) => Promise<void>, beforeUndo?: () => Promise<void>): Promise<[IBlock[], string]> {
|
||||||
const blocks = await octoClient.getSubtree(boardId, 3)
|
const blocks = await octoClient.getSubtree(boardId, 3)
|
||||||
const [newBlocks1, idMap] = OctoUtils.duplicateBlockTree(blocks, boardId)
|
const [newBlocks1, newBoard] = OctoUtils.duplicateBlockTree(blocks, boardId)
|
||||||
const newBlocks = newBlocks1.filter((o) => o.type !== 'comment')
|
const newBlocks = newBlocks1.filter((o) => o.type !== 'comment')
|
||||||
Utils.log(`duplicateBoard: duplicating ${newBlocks.length} blocks`)
|
Utils.log(`duplicateBoard: duplicating ${newBlocks.length} blocks`)
|
||||||
const newBoardId = idMap[boardId]
|
|
||||||
const newBoard = newBlocks.find((o) => o.id === newBoardId)!
|
|
||||||
newBoard.title = `Copy of ${newBoard.title}`
|
newBoard.title = `Copy of ${newBoard.title}`
|
||||||
await this.insertBlocks(
|
await this.insertBlocks(
|
||||||
newBlocks,
|
newBlocks,
|
||||||
description,
|
description,
|
||||||
async () => {
|
async () => {
|
||||||
await afterRedo?.(newBoardId)
|
await afterRedo?.(newBoard.id)
|
||||||
},
|
},
|
||||||
beforeUndo,
|
beforeUndo,
|
||||||
)
|
)
|
||||||
return [newBlocks, newBoardId]
|
return [newBlocks, newBoard.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other methods
|
// Other methods
|
||||||
|
@ -88,7 +88,7 @@ class OctoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Creates a copy of the blocks with new ids and parentIDs
|
// Creates a copy of the blocks with new ids and parentIDs
|
||||||
static duplicateBlockTree(blocks: IBlock[], rootBlockId?: string): [MutableBlock[], Readonly<Record<string, string>>] {
|
static duplicateBlockTree(blocks: IBlock[], rootBlockId: string): [MutableBlock[], MutableBlock, Readonly<Record<string, string>>] {
|
||||||
const idMap: Record<string, string> = {}
|
const idMap: Record<string, string> = {}
|
||||||
const newBlocks = blocks.map((block) => {
|
const newBlocks = blocks.map((block) => {
|
||||||
const newBlock = this.hydrateBlock(block)
|
const newBlock = this.hydrateBlock(block)
|
||||||
@ -97,7 +97,7 @@ class OctoUtils {
|
|||||||
return newBlock
|
return newBlock
|
||||||
})
|
})
|
||||||
|
|
||||||
const newRootBlockId = rootBlockId ? idMap[rootBlockId] : undefined
|
const newRootBlockId = idMap[rootBlockId]
|
||||||
newBlocks.forEach((newBlock) => {
|
newBlocks.forEach((newBlock) => {
|
||||||
// Note: Don't remap the parent of the new root block
|
// Note: Don't remap the parent of the new root block
|
||||||
if (newBlock.id !== newRootBlockId && newBlock.parentId) {
|
if (newBlock.id !== newRootBlockId && newBlock.parentId) {
|
||||||
@ -112,7 +112,8 @@ class OctoUtils {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return [newBlocks, idMap]
|
const newRootBlock = newBlocks.find((block) => block.id === newRootBlockId)!
|
||||||
|
return [newBlocks, newRootBlock, idMap]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ export default class BoardPage extends React.Component<Props, State> {
|
|||||||
|
|
||||||
const workspaceTree = new MutableWorkspaceTree()
|
const workspaceTree = new MutableWorkspaceTree()
|
||||||
await workspaceTree.sync()
|
await workspaceTree.sync()
|
||||||
const boardIds = workspaceTree.boards.map((o) => o.id)
|
const boardIds = [...workspaceTree.boards.map((o) => o.id), ...workspaceTree.boardTemplates.map((o) => o.id)]
|
||||||
this.setState({workspaceTree})
|
this.setState({workspaceTree})
|
||||||
|
|
||||||
// Listen to boards plus all blocks at root (Empty string for parentId)
|
// Listen to boards plus all blocks at root (Empty string for parentId)
|
||||||
|
@ -32,6 +32,7 @@ interface BoardTree {
|
|||||||
orderedCards(): Card[]
|
orderedCards(): Card[]
|
||||||
|
|
||||||
mutableCopy(): MutableBoardTree
|
mutableCopy(): MutableBoardTree
|
||||||
|
templateCopy(): MutableBoardTree
|
||||||
}
|
}
|
||||||
|
|
||||||
class MutableBoardTree implements BoardTree {
|
class MutableBoardTree implements BoardTree {
|
||||||
@ -405,6 +406,14 @@ class MutableBoardTree implements BoardTree {
|
|||||||
boardTree.incrementalUpdate(this.rawBlocks)
|
boardTree.incrementalUpdate(this.rawBlocks)
|
||||||
return boardTree
|
return boardTree
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templateCopy(): MutableBoardTree {
|
||||||
|
const [newBlocks, newBoard] = OctoUtils.duplicateBlockTree(this.allBlocks, this.board.id)
|
||||||
|
|
||||||
|
const boardTree = new MutableBoardTree(newBoard.id)
|
||||||
|
boardTree.incrementalUpdate(newBlocks)
|
||||||
|
return boardTree
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {MutableBoardTree, BoardTree, Group as BoardTreeGroup}
|
export {MutableBoardTree, BoardTree, Group as BoardTreeGroup}
|
||||||
|
@ -8,6 +8,7 @@ import {OctoUtils} from '../octoUtils'
|
|||||||
|
|
||||||
interface WorkspaceTree {
|
interface WorkspaceTree {
|
||||||
readonly boards: readonly Board[]
|
readonly boards: readonly Board[]
|
||||||
|
readonly boardTemplates: readonly Board[]
|
||||||
readonly views: readonly BoardView[]
|
readonly views: readonly BoardView[]
|
||||||
|
|
||||||
mutableCopy(): MutableWorkspaceTree
|
mutableCopy(): MutableWorkspaceTree
|
||||||
@ -15,6 +16,7 @@ interface WorkspaceTree {
|
|||||||
|
|
||||||
class MutableWorkspaceTree {
|
class MutableWorkspaceTree {
|
||||||
boards: Board[] = []
|
boards: Board[] = []
|
||||||
|
boardTemplates: Board[] = []
|
||||||
views: BoardView[] = []
|
views: BoardView[] = []
|
||||||
|
|
||||||
private rawBlocks: IBlock[] = []
|
private rawBlocks: IBlock[] = []
|
||||||
@ -37,7 +39,10 @@ class MutableWorkspaceTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private rebuild(blocks: IBlock[]) {
|
private rebuild(blocks: IBlock[]) {
|
||||||
this.boards = blocks.filter((block) => block.type === 'board').
|
const allBoards = blocks.filter((block) => block.type === 'board') as Board[]
|
||||||
|
this.boards = allBoards.filter((block) => !block.isTemplate).
|
||||||
|
sort((a, b) => a.title.localeCompare(b.title)) as Board[]
|
||||||
|
this.boardTemplates = allBoards.filter((block) => block.isTemplate).
|
||||||
sort((a, b) => a.title.localeCompare(b.title)) as Board[]
|
sort((a, b) => a.title.localeCompare(b.title)) as Board[]
|
||||||
this.views = blocks.filter((block) => block.type === 'view').
|
this.views = blocks.filter((block) => block.type === 'view').
|
||||||
sort((a, b) => a.title.localeCompare(b.title)) as BoardView[]
|
sort((a, b) => a.title.localeCompare(b.title)) as BoardView[]
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
.IconButton {
|
.IconButton {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
background-color: rgba(var(--main-fg), 0.1);
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
.Icon {
|
.Icon {
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
.SubmenuTriangleIcon {
|
.SubmenuTriangleIcon {
|
||||||
fill: rgba(var(--main-fg), 0.7);
|
fill: rgba(var(--main-fg), 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Icon {
|
.Icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
Loading…
Reference in New Issue
Block a user