1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-21 13:38:56 +02:00

Import from trello: comments, images, attachments

This commit is contained in:
omggga 2023-10-16 14:46:07 +03:00
parent 134422df4d
commit 599347b26d
4 changed files with 177 additions and 8 deletions

View File

@ -8,4 +8,99 @@ This subfolder contains scripts to import data from other systems. It is at an e
* Todoist
* 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.

View File

@ -6,6 +6,9 @@ import {exit} from 'process'
import {ArchiveUtils} from '../util/archive'
import {Block} from '../../webapp/src/blocks/block'
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 {createBoardView} from '../../webapp/src/blocks/boardView'
import {createCard} from '../../webapp/src/blocks/card'
@ -112,6 +115,8 @@ function convert(input: Trello): [Board[], Block[]] {
outCard.title = card.name
outCard.boardId = board.id
outCard.parentId = board.id
const updateDate = new Date(card.dateLastActivity)
outCard.updateAt = updateDate.getTime()
// Map lists to Select property options
if (card.idList) {
@ -127,6 +132,11 @@ function convert(input: Trello): [Board[], Block[]] {
blocks.push(outCard)
if (!outCard.fields.contentOrder) {
outCard.fields.contentOrder = []
}
//Description
if (card.desc) {
// console.log(`\t${card.desc}`)
const text = createTextBlock()
@ -134,16 +144,60 @@ function convert(input: Trello): [Board[], Block[]] {
text.boardId = board.id
text.parentId = outCard.id
blocks.push(text)
outCard.fields.contentOrder = [text.id]
outCard.fields.contentOrder.push(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
if (card.idChecklists && card.idChecklists.length > 0) {
card.idChecklists.forEach(checklistID => {
const lookup = input.checklists.find(e => e.id === checklistID)
const lookup = card.checklists.find(e => e.id === checklistID)
if (lookup) {
lookup.checkItems.forEach(trelloCheckBox=> {
lookup.checkItems.forEach((trelloCheckBox) => {
const checkBlock = createCheckboxBlock()
checkBlock.title = trelloCheckBox.name
if (trelloCheckBox.state === 'complete') {
@ -162,12 +216,26 @@ function convert(input: Trello): [Board[], Block[]] {
}
})
console.log('')
console.log(`Found ${input.cards.length} card(s).`)
console.log(`\nFound ${input.cards.length} card(s).`)
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() {
console.log('import -i <input.json> -o [output.boardarchive]')
exit(1)

View File

@ -70,6 +70,7 @@ export interface Icon {
}
export interface Data {
attachment: any;
old?: Old;
customField?: DataCustomField;
customFieldItem?: CustomFieldItem;
@ -306,6 +307,7 @@ export interface CardElement {
due: null | string;
email: string;
idChecklists: string[];
checklists: ChecklistElement[];
idMembers: IDMemberCreator[];
labels: Label[];
limits: CardLimits;

View File

@ -25,7 +25,10 @@ interface BoardArchiveLine extends ArchiveLine {
}
class ArchiveUtils {
static buildBlockArchive(boards: readonly Board[], blocks: readonly Block[]): string {
static buildBlockArchive(
boards: readonly Board[],
blocks: readonly Block[]
): string {
const header: ArchiveHeader = {
version: 1,
date: Date.now(),
@ -95,4 +98,5 @@ class ArchiveUtils {
}
}
export {ArchiveHeader, ArchiveLine, BlockArchiveLine, ArchiveUtils}
export {ArchiveHeader, ArchiveLine, BlockArchiveLine, ArchiveUtils}