diff --git a/server/api/controllers/boards/import.js b/server/api/controllers/boards/import.js new file mode 100755 index 00000000..8740f263 --- /dev/null +++ b/server/api/controllers/boards/import.js @@ -0,0 +1,87 @@ +const util = require('util'); +const { v4: uuid } = require('uuid'); + +const Errors = { + PROJECT_NOT_FOUND: { + projectNotFound: 'Project not found', + }, +}; + +module.exports = { + inputs: { + projectId: { + type: 'string', + regex: /^[0-9]+$/, + required: true, + }, + position: { + type: 'number', + required: true, + }, + name: { + type: 'string', + required: true, + }, + }, + + exits: { + projectNotFound: { + responseType: 'notFound', + }, + }, + + async fn(inputs) { + const { currentUser } = this.req; + + const upload = util.promisify((options, callback) => + this.req.file('file').upload(options, (error, files) => callback(error, files)), + ); + + let files; + try { + files = await upload({ + saveAs: uuid(), + maxBytes: null, + }); + } catch (error) { + return exits.uploadError(error.message); // TODO: add error + } + + const project = await Project.findOne(inputs.projectId); + + if (!project) { + throw Errors.PROJECT_NOT_FOUND; + } + + const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id); + + if (!isProjectManager) { + throw Errors.PROJECT_NOT_FOUND; // Forbidden + } + + const values = { + ..._.pick(inputs, ['position', 'name']), + type: 'kanban', + }; + + const { board, boardMembership } = await sails.helpers.boards.createOne( + values, + currentUser, + project, + this.req, + ); + + await sails.helpers.boards.importTrello(currentUser, board, files[0], this.req); + + if (this.req.isSocket) { + sails.sockets.join(this.req, `board:${board.id}`); // TODO: only when subscription needed + } + + return { + item: board, + included: { + boardMemberships: [boardMembership], + }, + }; + }, +}; diff --git a/server/api/helpers/boards/import-trello.js b/server/api/helpers/boards/import-trello.js new file mode 100644 index 00000000..ef1039f2 --- /dev/null +++ b/server/api/helpers/boards/import-trello.js @@ -0,0 +1,149 @@ +const fs = require('fs'); + +async function importFromTrello(inputs) { + let trelloBoard; + + const getTrelloLists = () => trelloBoard.lists.filter((list) => !list.closed); + const getTrelloCardsOfList = (listId) => + trelloBoard.cards.filter((l) => l.idList === listId && !l.closed); + const getAllTrelloCheckItemsOfCard = (cardId) => + trelloBoard.checklists + .filter((c) => c.idCard === cardId) + .map((checklist) => checklist.checkItems) + .flat(); + const getTrelloCommentsOfCard = (cardId) => + trelloBoard.actions.filter( + (action) => + action.type === 'commentCard' && + action.data && + action.data.card && + action.data.card.id === cardId, + ); + + const loadTrelloFile = async () => + new Promise((resolve, reject) => { + fs.readFile(inputs.trelloFile.fd, (err, data) => { + const exp = data && JSON.parse(data); + if (err) { + reject(err); + return; + } + trelloBoard = exp; + resolve(exp); + }); + }); + + const importComments = async (trelloCard, plankaCard) => { + const trelloComments = getTrelloCommentsOfCard(trelloCard.id); + trelloComments.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + return Promise.all( + trelloComments.map(async (trelloComment) => { + return sails.helpers.actions.createOne( + { + type: 'commentCard', + data: { + text: + `${trelloComment.data.text}\n\n---\n*Note: imported comment, originally posted by ` + + `\n${trelloComment.memberCreator.fullName} (${trelloComment.memberCreator.username}) on ${trelloComment.date}*`, + }, + }, + inputs.user, + plankaCard, + inputs.request, + ); + }), + ); + }; + + const importTasks = async (trelloCard, plankaCard) => { + // TODO find workaround for tasks/checklist mismapping, see issue trello2planka#5 + return Promise.all( + getAllTrelloCheckItemsOfCard(trelloCard.id).map(async (trelloCheckItem) => { + return sails.helpers.tasks.createOne( + { + cardId: plankaCard.id, + position: trelloCheckItem.pos, + name: trelloCheckItem.name, + isCompleted: trelloCheckItem.state === 'complete', + }, + plankaCard, + inputs.request, + ); + }), + ); + }; + + const importCards = async (trelloList, plankaList) => { + return Promise.all( + getTrelloCardsOfList(trelloList.id).map(async (trelloCard) => { + const plankaCard = await sails.helpers.cards.createOne( + { + listId: plankaList.id, + position: trelloCard.pos, + name: trelloCard.name, + description: trelloCard.desc || null, + }, + inputs.user, + inputs.board, + plankaList, + inputs.request, + ); + + await importTasks(trelloCard, plankaCard); + await importComments(trelloCard, plankaCard); + return plankaCard; + }), + ); + }; + + const importLists = async () => { + return Promise.all( + getTrelloLists().map(async (trelloList) => { + const plankaList = await sails.helpers.lists.createOne( + { + name: trelloList.name, + position: trelloList.pos, + }, + inputs.board, + inputs.request, + ); + return importCards(trelloList, plankaList); + }), + ); + }; + + await loadTrelloFile(); + await importLists(); +} + +module.exports = { + inputs: { + user: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + trelloFile: { + type: 'json', + required: true, + }, + request: { + type: 'ref', + }, + }, + + async fn(inputs) { + // TODO some validations or something? check if the input file is ok? + + await importFromTrello(inputs); + + // TODO handle errors properly + + return { + board: inputs.board, + }; + }, +}; diff --git a/server/config/routes.js b/server/config/routes.js index 05450eac..d830744e 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -33,6 +33,7 @@ module.exports.routes = { 'DELETE /api/project-managers/:id': 'project-managers/delete', 'POST /api/projects/:projectId/boards': 'boards/create', + 'POST /api/projects/:projectId/imports/boards': 'boards/import', 'GET /api/boards/:id': 'boards/show', 'PATCH /api/boards/:id': 'boards/update', 'DELETE /api/boards/:id': 'boards/delete',