2020-10-20 21:50:53 +02:00
|
|
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
|
|
// See LICENSE.txt for license information.
|
2021-03-02 22:21:55 +01:00
|
|
|
import {ArchiveUtils, IArchiveHeader, IArchiveLine, IBlockArchiveLine} from './blocks/archive'
|
|
|
|
import {IBlock, IMutableBlock} from './blocks/block'
|
2021-03-03 00:14:37 +01:00
|
|
|
import {LineReader} from './lineReader'
|
2020-10-20 21:52:56 +02:00
|
|
|
import mutator from './mutator'
|
|
|
|
import {Utils} from './utils'
|
2021-02-16 19:40:35 +01:00
|
|
|
import {BoardTree} from './viewModel/boardTree'
|
2020-10-08 18:21:27 +02:00
|
|
|
|
|
|
|
class Archiver {
|
2020-10-20 22:36:54 +02:00
|
|
|
static async exportBoardTree(boardTree: BoardTree): Promise<void> {
|
2020-10-20 21:50:53 +02:00
|
|
|
const blocks = boardTree.allBlocks
|
2021-03-02 22:21:55 +01:00
|
|
|
this.exportArchive(blocks)
|
2020-10-20 21:50:53 +02:00
|
|
|
}
|
|
|
|
|
2020-10-20 22:36:54 +02:00
|
|
|
static async exportFullArchive(): Promise<void> {
|
2020-10-20 21:50:53 +02:00
|
|
|
const blocks = await mutator.exportFullArchive()
|
2021-03-02 22:21:55 +01:00
|
|
|
this.exportArchive(blocks)
|
2020-10-20 21:50:53 +02:00
|
|
|
}
|
|
|
|
|
2021-03-02 22:21:55 +01:00
|
|
|
private static exportArchive(blocks: readonly IBlock[]): void {
|
|
|
|
const content = ArchiveUtils.buildBlockArchive(blocks)
|
2020-10-20 21:50:53 +02:00
|
|
|
|
|
|
|
const date = new Date()
|
2021-01-26 20:23:20 +01:00
|
|
|
const filename = `archive-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}.focalboard`
|
2020-10-20 21:50:53 +02:00
|
|
|
const link = document.createElement('a')
|
2020-10-20 21:52:56 +02:00
|
|
|
link.style.display = 'none'
|
2020-10-20 21:50:53 +02:00
|
|
|
|
|
|
|
// const file = new Blob([content], { type: "text/json" })
|
|
|
|
// link.href = URL.createObjectURL(file)
|
|
|
|
link.href = 'data:text/json,' + encodeURIComponent(content)
|
|
|
|
link.download = filename
|
|
|
|
document.body.appendChild(link) // FireFox support
|
|
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
|
|
// TODO: Remove or reuse link
|
|
|
|
}
|
|
|
|
|
2021-03-02 22:21:55 +01:00
|
|
|
private static async readBlocksFromFile(file: File): Promise<IBlock[]> {
|
|
|
|
const blocks: IBlock[] = []
|
|
|
|
|
2021-03-03 00:14:37 +01:00
|
|
|
let isFirstLine = true
|
|
|
|
return new Promise<IBlock[]>((resolve) => {
|
|
|
|
LineReader.readFile(file, (line, completed) => {
|
|
|
|
if (completed) {
|
|
|
|
resolve(blocks)
|
|
|
|
return
|
|
|
|
}
|
2021-03-02 22:21:55 +01:00
|
|
|
|
2021-03-03 00:14:37 +01:00
|
|
|
if (isFirstLine) {
|
|
|
|
isFirstLine = false
|
|
|
|
const header = JSON.parse(line) as IArchiveHeader
|
|
|
|
if (header.date && header.version >= 1) {
|
|
|
|
const date = new Date(header.date)
|
|
|
|
Utils.log(`Import archive, version: ${header.version}, date/time: ${date.toLocaleString()}.`)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const row = JSON.parse(line) as IArchiveLine
|
|
|
|
if (!row || !row.type || !row.data) {
|
2021-03-02 22:21:55 +01:00
|
|
|
Utils.logError('importFullArchive ERROR parsing line')
|
2021-03-03 00:14:37 +01:00
|
|
|
return
|
2021-03-02 22:21:55 +01:00
|
|
|
}
|
2021-03-03 00:14:37 +01:00
|
|
|
switch (row.type) {
|
2021-03-02 22:21:55 +01:00
|
|
|
case 'block': {
|
2021-03-03 00:14:37 +01:00
|
|
|
const blockLine = row as IBlockArchiveLine
|
2021-03-02 22:21:55 +01:00
|
|
|
const block = blockLine.data
|
|
|
|
blocks.push(block)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-03 00:14:37 +01:00
|
|
|
})
|
|
|
|
})
|
2021-03-02 22:21:55 +01:00
|
|
|
}
|
|
|
|
|
2020-10-20 21:50:53 +02:00
|
|
|
static importFullArchive(onComplete?: () => void): void {
|
|
|
|
const input = document.createElement('input')
|
2020-10-20 21:52:56 +02:00
|
|
|
input.type = 'file'
|
2021-01-26 20:23:20 +01:00
|
|
|
input.accept = '.focalboard'
|
2020-10-20 21:50:53 +02:00
|
|
|
input.onchange = async () => {
|
2020-11-13 02:24:24 +01:00
|
|
|
const file = input.files && input.files[0]
|
2021-03-02 22:21:55 +01:00
|
|
|
if (file) {
|
|
|
|
const blocks = await Archiver.readBlocksFromFile(file)
|
|
|
|
|
|
|
|
// Basic error checking
|
|
|
|
let filteredBlocks = blocks.filter((o) => Boolean(o.id))
|
2020-10-20 21:50:53 +02:00
|
|
|
|
2021-03-02 22:21:55 +01:00
|
|
|
Utils.log(`Import ${filteredBlocks.length} filtered blocks with ids.`)
|
2020-12-03 23:09:48 +01:00
|
|
|
|
2021-03-02 22:21:55 +01:00
|
|
|
this.fixRootIds(filteredBlocks)
|
2020-10-20 21:50:53 +02:00
|
|
|
|
2021-03-02 22:21:55 +01:00
|
|
|
filteredBlocks = filteredBlocks.filter((o) => Boolean(o.rootId))
|
2020-12-03 23:09:48 +01:00
|
|
|
|
2021-03-02 22:21:55 +01:00
|
|
|
Utils.log(`Import ${filteredBlocks.length} filtered blocks with rootIds.`)
|
2020-12-03 23:09:48 +01:00
|
|
|
|
2021-03-02 22:21:55 +01:00
|
|
|
await mutator.importFullArchive(filteredBlocks)
|
|
|
|
Utils.log('Import completed')
|
|
|
|
}
|
2020-10-20 21:50:53 +02:00
|
|
|
|
|
|
|
onComplete?.()
|
2020-10-20 21:52:56 +02:00
|
|
|
}
|
2020-10-20 21:50:53 +02:00
|
|
|
|
2020-10-20 21:52:56 +02:00
|
|
|
input.style.display = 'none'
|
2020-10-20 21:50:53 +02:00
|
|
|
document.body.appendChild(input)
|
|
|
|
input.click()
|
|
|
|
|
|
|
|
// TODO: Remove or reuse input
|
|
|
|
}
|
2020-12-03 23:09:48 +01:00
|
|
|
|
|
|
|
private static fixRootIds(blocks: IMutableBlock[]) {
|
|
|
|
const blockMap = new Map(blocks.map((o) => [o.id, o]))
|
|
|
|
const maxLevels = 5
|
|
|
|
for (let i = 0; i < maxLevels; i++) {
|
|
|
|
let missingRootIds = false
|
|
|
|
blocks.forEach((o) => {
|
|
|
|
if (o.parentId) {
|
|
|
|
const parent = blockMap.get(o.parentId)
|
|
|
|
if (parent) {
|
|
|
|
o.rootId = parent.rootId
|
|
|
|
} else {
|
|
|
|
Utils.assert(`No parent for ${o.type}: ${o.id} (${o.title})`)
|
|
|
|
}
|
|
|
|
if (!o.rootId) {
|
|
|
|
missingRootIds = true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
o.rootId = o.id
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!missingRootIds) {
|
|
|
|
Utils.log(`fixRootIds in ${i} levels`)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check and log remaining errors
|
|
|
|
blocks.forEach((o) => {
|
|
|
|
if (!o.rootId) {
|
|
|
|
const parent = blockMap.get(o.parentId)
|
|
|
|
Utils.logError(`RootId is null: ${o.type} ${o.id}, parentId ${o.parentId}: ${o.title}, parent: ${parent?.type}, parent.rootId: ${parent?.rootId}, parent.title: ${parent?.title}`)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2020-10-08 18:21:27 +02:00
|
|
|
}
|
|
|
|
|
2020-10-20 21:50:53 +02:00
|
|
|
export {Archiver}
|