mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-23 18:34:02 +02:00
Use JSONL format for archive
This commit is contained in:
parent
ad4d43698c
commit
5d050abd09
@ -3,7 +3,7 @@
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import minimist from 'minimist'
|
import minimist from 'minimist'
|
||||||
import {exit} from 'process'
|
import {exit} from 'process'
|
||||||
import {IArchive} from '../../webapp/src/blocks/archive'
|
import {ArchiveUtils} from '../../webapp/src/blocks/archive'
|
||||||
import {IBlock} from '../../webapp/src/blocks/block'
|
import {IBlock} from '../../webapp/src/blocks/block'
|
||||||
import {IPropertyOption, IPropertyTemplate, MutableBoard} from '../../webapp/src/blocks/board'
|
import {IPropertyOption, IPropertyTemplate, MutableBoard} from '../../webapp/src/blocks/board'
|
||||||
import {MutableBoardView} from '../../webapp/src/blocks/boardView'
|
import {MutableBoardView} from '../../webapp/src/blocks/boardView'
|
||||||
@ -49,10 +49,11 @@ function main() {
|
|||||||
const input = JSON.parse(inputData) as Asana
|
const input = JSON.parse(inputData) as Asana
|
||||||
|
|
||||||
// Convert
|
// Convert
|
||||||
const output = convert(input)
|
const blocks = convert(input)
|
||||||
|
|
||||||
// Save output
|
// Save output
|
||||||
const outputData = JSON.stringify(output)
|
// TODO: Stream output
|
||||||
|
const outputData = ArchiveUtils.buildBlockArchive(blocks)
|
||||||
fs.writeFileSync(outputFile, outputData)
|
fs.writeFileSync(outputFile, outputData)
|
||||||
|
|
||||||
console.log(`Exported to ${outputFile}`)
|
console.log(`Exported to ${outputFile}`)
|
||||||
@ -87,17 +88,11 @@ function getSections(input: Asana, projectId: string): Workspace[] {
|
|||||||
return [...sectionMap.values()]
|
return [...sectionMap.values()]
|
||||||
}
|
}
|
||||||
|
|
||||||
function convert(input: Asana): IArchive {
|
function convert(input: Asana): IBlock[] {
|
||||||
const archive: IArchive = {
|
|
||||||
version: 1,
|
|
||||||
date: Date.now(),
|
|
||||||
blocks: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const projects = getProjects(input)
|
const projects = getProjects(input)
|
||||||
if (projects.length < 1) {
|
if (projects.length < 1) {
|
||||||
console.error('No projects found')
|
console.error('No projects found')
|
||||||
return archive
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle multiple projects
|
// TODO: Handle multiple projects
|
||||||
@ -181,12 +176,10 @@ function convert(input: Asana): IArchive {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
archive.blocks = blocks
|
|
||||||
|
|
||||||
console.log('')
|
console.log('')
|
||||||
console.log(`Found ${input.data.length} card(s).`)
|
console.log(`Found ${input.data.length} card(s).`)
|
||||||
|
|
||||||
return archive
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint --ext .tsx,.ts . --quiet --cache",
|
"lint": "eslint --ext .tsx,.ts . --quiet --cache",
|
||||||
"fix": "eslint --ext .tsx,.ts . --quiet --fix --cache",
|
"fix": "eslint --ext .tsx,.ts . --quiet --fix --cache",
|
||||||
"test": "ts-node importAsana.ts -i test/asana.json -o test/archive.focalboard",
|
"test": "ts-node importAsana.ts -i test/asana.json -o test/asana-import.focalboard",
|
||||||
"debug:test": "node --inspect=5858 -r ts-node/register importAsana.ts -i test/asana.json -o test/archive.focalboard"
|
"debug:test": "node --inspect=5858 -r ts-node/register importAsana.ts -i test/asana.json -o test/asana-import.focalboard"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -3,7 +3,7 @@ import * as fs from 'fs'
|
|||||||
import minimist from 'minimist'
|
import minimist from 'minimist'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import {exit} from 'process'
|
import {exit} from 'process'
|
||||||
import {IArchive} from '../../webapp/src/blocks/archive'
|
import {ArchiveUtils} from '../../webapp/src/blocks/archive'
|
||||||
import {IBlock} from '../../webapp/src/blocks/block'
|
import {IBlock} from '../../webapp/src/blocks/block'
|
||||||
import {IPropertyTemplate, MutableBoard} from '../../webapp/src/blocks/board'
|
import {IPropertyTemplate, MutableBoard} from '../../webapp/src/blocks/board'
|
||||||
import {MutableBoardView} from '../../webapp/src/blocks/boardView'
|
import {MutableBoardView} from '../../webapp/src/blocks/boardView'
|
||||||
@ -70,10 +70,11 @@ async function main() {
|
|||||||
markdownFolder = path.join(inputFolder, basename)
|
markdownFolder = path.join(inputFolder, basename)
|
||||||
|
|
||||||
// Convert
|
// Convert
|
||||||
const output = convert(input, title)
|
const blocks = convert(input, title)
|
||||||
|
|
||||||
// Save output
|
// Save output
|
||||||
const outputData = JSON.stringify(output)
|
// TODO: Stream output
|
||||||
|
const outputData = ArchiveUtils.buildBlockArchive(blocks)
|
||||||
fs.writeFileSync(outputFile, outputData)
|
fs.writeFileSync(outputFile, outputData)
|
||||||
|
|
||||||
console.log(`Exported to ${outputFile}`)
|
console.log(`Exported to ${outputFile}`)
|
||||||
@ -115,15 +116,9 @@ function getColumns(input: any[]) {
|
|||||||
return keys.slice(1)
|
return keys.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function convert(input: any[], title: string): IArchive {
|
function convert(input: any[], title: string): IBlock[] {
|
||||||
const blocks: IBlock[] = []
|
const blocks: IBlock[] = []
|
||||||
|
|
||||||
const archive: IArchive = {
|
|
||||||
version: 1,
|
|
||||||
date: Date.now(),
|
|
||||||
blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
// Board
|
// Board
|
||||||
const board = new MutableBoard()
|
const board = new MutableBoard()
|
||||||
console.log(`Board: ${title}`)
|
console.log(`Board: ${title}`)
|
||||||
@ -160,7 +155,7 @@ function convert(input: any[], title: string): IArchive {
|
|||||||
console.log(keys)
|
console.log(keys)
|
||||||
if (keys.length < 1) {
|
if (keys.length < 1) {
|
||||||
console.error(`Expected at least one column`)
|
console.error(`Expected at least one column`)
|
||||||
return archive
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
const titleKey = keys[0]
|
const titleKey = keys[0]
|
||||||
@ -216,7 +211,7 @@ function convert(input: any[], title: string): IArchive {
|
|||||||
console.log('')
|
console.log('')
|
||||||
console.log(`Found ${input.length} card(s).`)
|
console.log(`Found ${input.length} card(s).`)
|
||||||
|
|
||||||
return archive
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint --ext .tsx,.ts . --quiet --cache",
|
"lint": "eslint --ext .tsx,.ts . --quiet --cache",
|
||||||
"fix": "eslint --ext .tsx,.ts . --quiet --fix --cache",
|
"fix": "eslint --ext .tsx,.ts . --quiet --fix --cache",
|
||||||
"test": "ts-node importNotion.ts -i test/export -o test/archive.focalboard",
|
"test": "ts-node importNotion.ts -i test/export -o test/notion-import.focalboard",
|
||||||
"debug:test": "node --inspect=5858 -r ts-node/register importNotion.ts -i test/export -o test/archive.focalboard"
|
"debug:test": "node --inspect=5858 -r ts-node/register importNotion.ts -i test/export -o test/notion-import.focalboard"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import minimist from 'minimist'
|
import minimist from 'minimist'
|
||||||
import {exit} from 'process'
|
import {exit} from 'process'
|
||||||
import {IArchive} from '../../webapp/src/blocks/archive'
|
import {ArchiveUtils} from '../../webapp/src/blocks/archive'
|
||||||
import {IBlock} from '../../webapp/src/blocks/block'
|
import {IBlock} from '../../webapp/src/blocks/block'
|
||||||
import {IPropertyOption, IPropertyTemplate, MutableBoard} from '../../webapp/src/blocks/board'
|
import {IPropertyOption, IPropertyTemplate, MutableBoard} from '../../webapp/src/blocks/board'
|
||||||
import {MutableBoardView} from '../../webapp/src/blocks/boardView'
|
import {MutableBoardView} from '../../webapp/src/blocks/boardView'
|
||||||
@ -49,16 +49,17 @@ function main() {
|
|||||||
const input = JSON.parse(inputData) as Trello
|
const input = JSON.parse(inputData) as Trello
|
||||||
|
|
||||||
// Convert
|
// Convert
|
||||||
const output = convert(input)
|
const blocks = convert(input)
|
||||||
|
|
||||||
// Save output
|
// Save output
|
||||||
const outputData = JSON.stringify(output)
|
// TODO: Stream output
|
||||||
|
const outputData = ArchiveUtils.buildBlockArchive(blocks)
|
||||||
fs.writeFileSync(outputFile, outputData)
|
fs.writeFileSync(outputFile, outputData)
|
||||||
|
|
||||||
console.log(`Exported to ${outputFile}`)
|
console.log(`Exported to ${outputFile}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function convert(input: Trello): IArchive {
|
function convert(input: Trello): IBlock[] {
|
||||||
const blocks: IBlock[] = []
|
const blocks: IBlock[] = []
|
||||||
|
|
||||||
// Board
|
// Board
|
||||||
@ -136,16 +137,10 @@ function convert(input: Trello): IArchive {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const archive: IArchive = {
|
|
||||||
version: 1,
|
|
||||||
date: Date.now(),
|
|
||||||
blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('')
|
console.log('')
|
||||||
console.log(`Found ${input.cards.length} card(s).`)
|
console.log(`Found ${input.cards.length} card(s).`)
|
||||||
|
|
||||||
return archive
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint --ext .tsx,.ts . --quiet --cache",
|
"lint": "eslint --ext .tsx,.ts . --quiet --cache",
|
||||||
"fix": "eslint --ext .tsx,.ts . --quiet --fix --cache",
|
"fix": "eslint --ext .tsx,.ts . --quiet --fix --cache",
|
||||||
"test": "ts-node importTrello.ts -i test/trello.json -o test/archive.focalboard",
|
"test": "ts-node importTrello.ts -i test/trello.json -o test/trello-import.focalboard",
|
||||||
"debug:test": "node --inspect=5858 -r ts-node/register importTrello.ts -i test/trello.json -o test/archive.focalboard"
|
"debug:test": "node --inspect=5858 -r ts-node/register importTrello.ts -i test/trello.json -o test/trello-import.focalboard"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -1,7 +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 {IArchive} from './blocks/archive'
|
import {ArchiveUtils, IArchiveHeader, IArchiveLine, IBlockArchiveLine} from './blocks/archive'
|
||||||
import {IMutableBlock} from './blocks/block'
|
import {IBlock, IMutableBlock} from './blocks/block'
|
||||||
import mutator from './mutator'
|
import mutator from './mutator'
|
||||||
import {Utils} from './utils'
|
import {Utils} from './utils'
|
||||||
import {BoardTree} from './viewModel/boardTree'
|
import {BoardTree} from './viewModel/boardTree'
|
||||||
@ -9,28 +9,16 @@ import {BoardTree} from './viewModel/boardTree'
|
|||||||
class Archiver {
|
class Archiver {
|
||||||
static async exportBoardTree(boardTree: BoardTree): Promise<void> {
|
static async exportBoardTree(boardTree: BoardTree): Promise<void> {
|
||||||
const blocks = boardTree.allBlocks
|
const blocks = boardTree.allBlocks
|
||||||
const archive: IArchive = {
|
this.exportArchive(blocks)
|
||||||
version: 1,
|
|
||||||
date: Date.now(),
|
|
||||||
blocks,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.exportArchive(archive)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async exportFullArchive(): Promise<void> {
|
static async exportFullArchive(): Promise<void> {
|
||||||
const blocks = await mutator.exportFullArchive()
|
const blocks = await mutator.exportFullArchive()
|
||||||
const archive: IArchive = {
|
this.exportArchive(blocks)
|
||||||
version: 1,
|
|
||||||
date: Date.now(),
|
|
||||||
blocks,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.exportArchive(archive)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static exportArchive(archive: IArchive): void {
|
private static exportArchive(blocks: readonly IBlock[]): void {
|
||||||
const content = JSON.stringify(archive)
|
const content = ArchiveUtils.buildBlockArchive(blocks)
|
||||||
|
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const filename = `archive-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}.focalboard`
|
const filename = `archive-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}.focalboard`
|
||||||
@ -48,32 +36,73 @@ class Archiver {
|
|||||||
// TODO: Remove or reuse link
|
// TODO: Remove or reuse link
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async readBlocksFromFile(file: File): Promise<IBlock[]> {
|
||||||
|
// TODO: Read input as a stream, line by line
|
||||||
|
const contents = await (new Response(file)).text()
|
||||||
|
Utils.log(`Import ${contents.length} bytes.`)
|
||||||
|
|
||||||
|
const blocks: IBlock[] = []
|
||||||
|
const allLineStrings = contents.split('\n')
|
||||||
|
if (allLineStrings.length >= 2) {
|
||||||
|
const headerString = allLineStrings[0]
|
||||||
|
const header = JSON.parse(headerString) 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()}, ${blocks.length} block(s).`)
|
||||||
|
|
||||||
|
const lineStrings = allLineStrings.slice(1)
|
||||||
|
for (const lineString of lineStrings) {
|
||||||
|
if (!lineString) {
|
||||||
|
// Ignore empty lines, e.g. last line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = JSON.parse(lineString) as IArchiveLine
|
||||||
|
if (!line || !line.type || !line.data) {
|
||||||
|
Utils.logError('importFullArchive ERROR parsing line')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch (line.type) {
|
||||||
|
case 'block': {
|
||||||
|
const blockLine = line as IBlockArchiveLine
|
||||||
|
const block = blockLine.data
|
||||||
|
blocks.push(block)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Utils.logError('importFullArchive ERROR parsing header')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
static importFullArchive(onComplete?: () => void): void {
|
static importFullArchive(onComplete?: () => void): void {
|
||||||
const input = document.createElement('input')
|
const input = document.createElement('input')
|
||||||
input.type = 'file'
|
input.type = 'file'
|
||||||
input.accept = '.focalboard'
|
input.accept = '.focalboard'
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
const file = input.files && input.files[0]
|
const file = input.files && input.files[0]
|
||||||
const contents = await (new Response(file)).text()
|
if (file) {
|
||||||
Utils.log(`Import ${contents.length} bytes.`)
|
const blocks = await Archiver.readBlocksFromFile(file)
|
||||||
const archive: IArchive = JSON.parse(contents)
|
|
||||||
const {blocks} = archive
|
|
||||||
const date = new Date(archive.date)
|
|
||||||
Utils.log(`Import archive, version: ${archive.version}, date/time: ${date.toLocaleString()}, ${blocks.length} block(s).`)
|
|
||||||
|
|
||||||
// Basic error checking
|
// Basic error checking
|
||||||
let filteredBlocks = blocks.filter((o) => Boolean(o.id))
|
let filteredBlocks = blocks.filter((o) => Boolean(o.id))
|
||||||
|
|
||||||
Utils.log(`Import ${filteredBlocks.length} filtered blocks with ids.`)
|
Utils.log(`Import ${filteredBlocks.length} filtered blocks with ids.`)
|
||||||
|
|
||||||
this.fixRootIds(filteredBlocks)
|
this.fixRootIds(filteredBlocks)
|
||||||
|
|
||||||
filteredBlocks = filteredBlocks.filter((o) => Boolean(o.rootId))
|
filteredBlocks = filteredBlocks.filter((o) => Boolean(o.rootId))
|
||||||
|
|
||||||
Utils.log(`Import ${filteredBlocks.length} filtered blocks with rootIds.`)
|
Utils.log(`Import ${filteredBlocks.length} filtered blocks with rootIds.`)
|
||||||
|
|
||||||
|
await mutator.importFullArchive(filteredBlocks)
|
||||||
|
Utils.log('Import completed')
|
||||||
|
}
|
||||||
|
|
||||||
await mutator.importFullArchive(filteredBlocks)
|
|
||||||
Utils.log('Import completed')
|
|
||||||
onComplete?.()
|
onComplete?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,43 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import {IBlock} from './block'
|
import {IBlock} from './block'
|
||||||
|
|
||||||
interface IArchive {
|
interface IArchiveHeader {
|
||||||
version: number
|
version: number
|
||||||
date: number
|
date: number
|
||||||
blocks: readonly IBlock[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {IArchive}
|
interface IArchiveLine {
|
||||||
|
type: string,
|
||||||
|
data: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This schema allows the expansion of additional line types in the future
|
||||||
|
interface IBlockArchiveLine extends IArchiveLine {
|
||||||
|
type: 'block',
|
||||||
|
data: IBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArchiveUtils {
|
||||||
|
static buildBlockArchive(blocks: readonly IBlock[]): string {
|
||||||
|
const header: IArchiveHeader = {
|
||||||
|
version: 1,
|
||||||
|
date: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerString = JSON.stringify(header)
|
||||||
|
let content = headerString + '\n'
|
||||||
|
for (const block of blocks) {
|
||||||
|
const line: IBlockArchiveLine = {
|
||||||
|
type: 'block',
|
||||||
|
data: block,
|
||||||
|
}
|
||||||
|
const lineString = JSON.stringify(line)
|
||||||
|
content += lineString
|
||||||
|
content += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {IArchiveHeader, IArchiveLine, IBlockArchiveLine, ArchiveUtils}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user