mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-02 14:47:55 +02:00
Import from trello: comments, images, attachments
This commit is contained in:
parent
134422df4d
commit
599347b26d
@ -8,4 +8,99 @@ This subfolder contains scripts to import data from other systems. It is at an e
|
|||||||
* Todoist
|
* Todoist
|
||||||
* Nextcloud Deck
|
* Nextcloud Deck
|
||||||
|
|
||||||
|
## Trello
|
||||||
|
|
||||||
|
The base structure for importing archive is the same for every method:
|
||||||
|
|
||||||
|
```
|
||||||
|
- board.boardarchive: a normal zip archive with changed extension
|
||||||
|
-- version.json version metadata
|
||||||
|
-- {boardId}: folder with name equal to boardId
|
||||||
|
--- board.jsonl jsonl board metadata generated for board with importTrello.ts
|
||||||
|
--- {attachments} attachments from trello in format {trello_attachment_id}.{extensions}
|
||||||
|
```
|
||||||
|
|
||||||
|
To create board.jsonl use *.json board data file from trello and command:
|
||||||
|
```
|
||||||
|
node -r ts-node/register importTrello.ts -i {trello_board_data}.json -o board.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
Add attachments to the {boardId} folder, the fastest way is to download them thourgh the same {trello_board_data}.json (you can get origin json from trello export option) with your API keys, the name pattern is `${trello_attachment_id}.${fileExtension}`:
|
||||||
|
<details>
|
||||||
|
<summary>NodeJS example</summary>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const fs = require('fs');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const API_KEY = 'XXXXXX';
|
||||||
|
const TOKEN = 'XXXXXX';
|
||||||
|
const baseURL = 'https://api.trello.com/1';
|
||||||
|
|
||||||
|
async function downloadFile(metaUrl, dest, card) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `OAuth oauth_consumer_key="${API_KEY}", oauth_token="${TOKEN}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await fetch(metaUrl, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch metadata from ${metaUrl}. Status: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = await response.json();
|
||||||
|
|
||||||
|
// Now, fetch the actual file using the provided download format
|
||||||
|
const fileUrl = `https://api.trello.com/1/cards/${card.id}/attachments/${metadata.id}/download/${metadata.fileName}`;
|
||||||
|
response = await fetch(fileUrl, { headers });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch file from ${fileUrl}. Status: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await response.buffer();
|
||||||
|
await fs.promises.writeFile(dest, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync('data.json', 'utf8'));
|
||||||
|
|
||||||
|
if (!data.cards || data.cards.length === 0) {
|
||||||
|
console.log("No cards found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Cards: ' + data.cards.length)
|
||||||
|
|
||||||
|
for (const card of data.cards) {
|
||||||
|
if (!card.attachments || card.attachments.length === 0) {
|
||||||
|
console.log(`Card ${card.id} has no attachments.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const attachment of card.attachments) {
|
||||||
|
const fileExtension = path.extname(attachment.fileName || '');
|
||||||
|
const fileName = `${attachment.id}${fileExtension}`;
|
||||||
|
|
||||||
|
// Build the Trello API URL
|
||||||
|
const downloadUrl = `${baseURL}/cards/${card.id}/attachments/${attachment.id}?key=${API_KEY}&token=${TOKEN}`;
|
||||||
|
try {
|
||||||
|
await downloadFile(downloadUrl, `./downloads/${fileName}`, card);
|
||||||
|
console.log(`File saved as ./downloads/${fileName}`);
|
||||||
|
} catch (err){
|
||||||
|
//Sometimes attachs cannot be downloaded
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
[Contribute code](https://mattermost.github.io/focalboard/) to expand this.
|
[Contribute code](https://mattermost.github.io/focalboard/) to expand this.
|
||||||
|
@ -6,6 +6,9 @@ import {exit} from 'process'
|
|||||||
import {ArchiveUtils} from '../util/archive'
|
import {ArchiveUtils} from '../util/archive'
|
||||||
import {Block} from '../../webapp/src/blocks/block'
|
import {Block} from '../../webapp/src/blocks/block'
|
||||||
import {Board} from '../../webapp/src/blocks/board'
|
import {Board} from '../../webapp/src/blocks/board'
|
||||||
|
import {createAttachmentBlock} from '../../webapp/src/blocks/attachmentBlock'
|
||||||
|
import {createImageBlock} from '../../webapp/src/blocks/imageBlock'
|
||||||
|
import {createCommentBlock} from '../../webapp/src/blocks/commentBlock'
|
||||||
import {IPropertyOption, IPropertyTemplate, createBoard} from '../../webapp/src/blocks/board'
|
import {IPropertyOption, IPropertyTemplate, createBoard} from '../../webapp/src/blocks/board'
|
||||||
import {createBoardView} from '../../webapp/src/blocks/boardView'
|
import {createBoardView} from '../../webapp/src/blocks/boardView'
|
||||||
import {createCard} from '../../webapp/src/blocks/card'
|
import {createCard} from '../../webapp/src/blocks/card'
|
||||||
@ -112,6 +115,8 @@ function convert(input: Trello): [Board[], Block[]] {
|
|||||||
outCard.title = card.name
|
outCard.title = card.name
|
||||||
outCard.boardId = board.id
|
outCard.boardId = board.id
|
||||||
outCard.parentId = board.id
|
outCard.parentId = board.id
|
||||||
|
const updateDate = new Date(card.dateLastActivity)
|
||||||
|
outCard.updateAt = updateDate.getTime()
|
||||||
|
|
||||||
// Map lists to Select property options
|
// Map lists to Select property options
|
||||||
if (card.idList) {
|
if (card.idList) {
|
||||||
@ -127,6 +132,11 @@ function convert(input: Trello): [Board[], Block[]] {
|
|||||||
|
|
||||||
blocks.push(outCard)
|
blocks.push(outCard)
|
||||||
|
|
||||||
|
if (!outCard.fields.contentOrder) {
|
||||||
|
outCard.fields.contentOrder = []
|
||||||
|
}
|
||||||
|
|
||||||
|
//Description
|
||||||
if (card.desc) {
|
if (card.desc) {
|
||||||
// console.log(`\t${card.desc}`)
|
// console.log(`\t${card.desc}`)
|
||||||
const text = createTextBlock()
|
const text = createTextBlock()
|
||||||
@ -134,16 +144,60 @@ function convert(input: Trello): [Board[], Block[]] {
|
|||||||
text.boardId = board.id
|
text.boardId = board.id
|
||||||
text.parentId = outCard.id
|
text.parentId = outCard.id
|
||||||
blocks.push(text)
|
blocks.push(text)
|
||||||
|
outCard.fields.contentOrder.push(text.id)
|
||||||
outCard.fields.contentOrder = [text.id]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attachments
|
||||||
|
if (card.attachments){
|
||||||
|
card.attachments.forEach(attach => {
|
||||||
|
const attachment = createAttachmentBlock()
|
||||||
|
const extension = getFileExtension(attach.name)
|
||||||
|
const name = `${attach.id}.${extension}`
|
||||||
|
attachment.fields.fileId = name
|
||||||
|
attachment.boardId = board.id
|
||||||
|
attachment.parentId = outCard.id
|
||||||
|
attachment.title = name
|
||||||
|
|
||||||
|
const date = new Date(attach.date)
|
||||||
|
attachment.createAt = date.getTime()
|
||||||
|
blocks.push(attachment)
|
||||||
|
|
||||||
|
if (isImageFile(name)){
|
||||||
|
const image = createImageBlock()
|
||||||
|
image.boardId = board.id
|
||||||
|
image.parentId = outCard.id
|
||||||
|
image.fields.fileId = name
|
||||||
|
blocks.push(image)
|
||||||
|
outCard.fields.contentOrder.push(image.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Iteratin actions to find comments and card createdBy
|
||||||
|
input.actions.forEach(action => {
|
||||||
|
if (action.data.card && action.data.card.id === card.id) {
|
||||||
|
if (action.type === 'createCard') {
|
||||||
|
const date = new Date(action.date)
|
||||||
|
outCard.createAt = date.getTime()
|
||||||
|
} else if (action.type === 'commentCard') {
|
||||||
|
const comment = createCommentBlock()
|
||||||
|
comment.boardId = board.id
|
||||||
|
comment.parentId = outCard.id
|
||||||
|
const date = new Date(action.date)
|
||||||
|
comment.createAt = date.getTime()
|
||||||
|
comment.title = action.data.text!
|
||||||
|
blocks.push(comment)
|
||||||
|
outCard.fields.contentOrder.push(comment.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Add Checklists
|
// Add Checklists
|
||||||
if (card.idChecklists && card.idChecklists.length > 0) {
|
if (card.idChecklists && card.idChecklists.length > 0) {
|
||||||
card.idChecklists.forEach(checklistID => {
|
card.idChecklists.forEach(checklistID => {
|
||||||
const lookup = input.checklists.find(e => e.id === checklistID)
|
const lookup = card.checklists.find(e => e.id === checklistID)
|
||||||
if (lookup) {
|
if (lookup) {
|
||||||
lookup.checkItems.forEach(trelloCheckBox=> {
|
lookup.checkItems.forEach((trelloCheckBox) => {
|
||||||
const checkBlock = createCheckboxBlock()
|
const checkBlock = createCheckboxBlock()
|
||||||
checkBlock.title = trelloCheckBox.name
|
checkBlock.title = trelloCheckBox.name
|
||||||
if (trelloCheckBox.state === 'complete') {
|
if (trelloCheckBox.state === 'complete') {
|
||||||
@ -162,12 +216,26 @@ function convert(input: Trello): [Board[], Block[]] {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('')
|
console.log(`\nFound ${input.cards.length} card(s).`)
|
||||||
console.log(`Found ${input.cards.length} card(s).`)
|
|
||||||
|
|
||||||
return [boards, blocks]
|
return [boards, blocks]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isImageFile(filename: string): boolean {
|
||||||
|
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tiff', 'svg']
|
||||||
|
const extension = filename.split('.').pop()?.toLowerCase()
|
||||||
|
return imageExtensions.includes(extension!)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileExtension(filename: string): string | null {
|
||||||
|
const parts = filename.split('.')
|
||||||
|
if (parts.length > 1) {
|
||||||
|
return parts[parts.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
console.log('import -i <input.json> -o [output.boardarchive]')
|
console.log('import -i <input.json> -o [output.boardarchive]')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
@ -70,6 +70,7 @@ export interface Icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
|
attachment: any;
|
||||||
old?: Old;
|
old?: Old;
|
||||||
customField?: DataCustomField;
|
customField?: DataCustomField;
|
||||||
customFieldItem?: CustomFieldItem;
|
customFieldItem?: CustomFieldItem;
|
||||||
@ -306,6 +307,7 @@ export interface CardElement {
|
|||||||
due: null | string;
|
due: null | string;
|
||||||
email: string;
|
email: string;
|
||||||
idChecklists: string[];
|
idChecklists: string[];
|
||||||
|
checklists: ChecklistElement[];
|
||||||
idMembers: IDMemberCreator[];
|
idMembers: IDMemberCreator[];
|
||||||
labels: Label[];
|
labels: Label[];
|
||||||
limits: CardLimits;
|
limits: CardLimits;
|
||||||
|
@ -25,7 +25,10 @@ interface BoardArchiveLine extends ArchiveLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ArchiveUtils {
|
class ArchiveUtils {
|
||||||
static buildBlockArchive(boards: readonly Board[], blocks: readonly Block[]): string {
|
static buildBlockArchive(
|
||||||
|
boards: readonly Board[],
|
||||||
|
blocks: readonly Block[]
|
||||||
|
): string {
|
||||||
const header: ArchiveHeader = {
|
const header: ArchiveHeader = {
|
||||||
version: 1,
|
version: 1,
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
@ -96,3 +99,4 @@ class ArchiveUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {ArchiveHeader, ArchiveLine, BlockArchiveLine, ArchiveUtils}
|
export {ArchiveHeader, ArchiveLine, BlockArchiveLine, ArchiveUtils}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user