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:
parent
134422df4d
commit
599347b26d
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user