Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import from trello: comments, images, attachments #4926

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions import/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
78 changes: 73 additions & 5 deletions import/trello/importTrello.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand All @@ -127,23 +132,72 @@ 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()
text.title = card.desc
text.boardId = board.id
text.parentId = outCard.id
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
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') {
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions import/trello/trello.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface Icon {
}

export interface Data {
attachment: any;
old?: Old;
customField?: DataCustomField;
customFieldItem?: CustomFieldItem;
Expand Down Expand Up @@ -306,6 +307,7 @@ export interface CardElement {
due: null | string;
email: string;
idChecklists: string[];
checklists: ChecklistElement[];
idMembers: IDMemberCreator[];
labels: Label[];
limits: CardLimits;
Expand Down
8 changes: 6 additions & 2 deletions import/util/archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -95,4 +98,5 @@ class ArchiveUtils {
}
}

export {ArchiveHeader, ArchiveLine, BlockArchiveLine, ArchiveUtils}
export {ArchiveHeader, ArchiveLine, BlockArchiveLine, ArchiveUtils}