From 468694a84cc164e4923f2d2e4631c37ceb1c4b55 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Thu, 15 Oct 2015 14:01:13 +0200 Subject: [PATCH 1/8] Import board: added UI --- client/components/boards/boardHeader.jade | 2 + client/components/boards/boardHeader.js | 1 + client/components/boards/boardHeader.styl | 2 + client/components/import/import.jade | 8 +++ client/components/import/import.js | 80 +++++++++++++++++++++++ client/components/lists/listHeader.jade | 9 --- client/components/lists/listHeader.js | 39 ----------- i18n/en.i18n.json | 8 ++- models/import.js | 25 +++++-- 9 files changed, 119 insertions(+), 55 deletions(-) create mode 100644 client/components/boards/boardHeader.styl create mode 100644 client/components/import/import.jade create mode 100644 client/components/import/import.js diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index ffc791435..e460170b9 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -107,6 +107,8 @@ template(name="createBoardPopup") | {{{_ 'board-private-info'}}} a.js-change-visibility {{_ 'change'}}. input.primary.wide(type="submit" value="{{_ 'create'}}") + | {{_ 'or'}} + a.js-import {{_ 'import-board'}} template(name="boardChangeTitlePopup") diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index dbd768952..92d5f6d45 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -145,6 +145,7 @@ BlazeComponent.extendComponent({ this.setVisibility(this.currentData()); }, 'click .js-change-visibility': this.toggleVisibilityMenu, + 'click .js-import': Popup.open('boardImportBoard'), submit: this.onSubmit, }]; }, diff --git a/client/components/boards/boardHeader.styl b/client/components/boards/boardHeader.styl new file mode 100644 index 000000000..adfe4b19e --- /dev/null +++ b/client/components/boards/boardHeader.styl @@ -0,0 +1,2 @@ +a.js-import + text-decoration underline diff --git a/client/components/import/import.jade b/client/components/import/import.jade new file mode 100644 index 000000000..8059b65b3 --- /dev/null +++ b/client/components/import/import.jade @@ -0,0 +1,8 @@ +template(name="importPopup") + if error.get + .warning {{_ error.get}} + form + label + | {{_ getLabel}} + textarea.js-card-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus) + input.primary.wide(type="submit" value="{{_ 'import'}}") diff --git a/client/components/import/import.js b/client/components/import/import.js new file mode 100644 index 000000000..f15185edb --- /dev/null +++ b/client/components/import/import.js @@ -0,0 +1,80 @@ +/** + * Abstract root for all import popup screens. + * Descendants must define: + * - getMethodName(): return the Meteor method to call for import, passing json data decoded as object + * and additional data (see below) + * - getAdditionalData(): return object containing additional data passed to Meteor method + * (like list ID and position for a card import) + * - getLabel(): i18n key for the text displayed in the popup, usually to explain how to get the data out of the + * source system. + */ +const ImportPopup = BlazeComponent.extendComponent({ + template() {return 'importPopup';}, + events() { + return [{ + 'submit': (evt) => { + evt.preventDefault(); + const dataJson = $(evt.currentTarget).find('textarea').val(); + let dataObject; + try { + dataObject = JSON.parse(dataJson); + } catch (e) { + this.setError('error-json-malformed'); + return; + } + Meteor.call(this.getMethodName(), dataObject, this.getAdditionalData(), + (error, response) => { + if (error) { + this.setError(error.error); + } else { + Filter.addException(response); + Popup.close(); + } + } + ); + }, + }]; + }, + + onCreated() { + this.error = new ReactiveVar(''); + }, + + setError(error) { + this.error.set(error); + }, +}); + +ImportPopup.extendComponent({ + getAdditionalData() { + const listId = this.data()._id; + const firstCardDom = $(`#js-list-${this.currentData()._id} .js-minicard:first`).get(0); + const sortIndex = Utils.calculateIndex(null, firstCardDom).base; + const result = {listId, sortIndex}; + return result; + }, + + getMethodName() { + return 'importTrelloCard'; + }, + + getLabel() { + return 'import-card-trello-instruction'; + }, +}).register('listImportCardPopup'); + +ImportPopup.extendComponent({ + getAdditionalData() { + const result = {}; + return result; + }, + + getMethodName() { + return 'importTrelloBoard'; + }, + + getLabel() { + return 'import-board-trello-instruction'; + }, +}).register('boardImportBoardPopup'); + diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index e7b16912b..72cd0fe97 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -31,15 +31,6 @@ template(name="listActionPopup") template(name="listMoveCardsPopup") +boardLists -template(name="listImportCardPopup") - if error.get - .warning {{_ error.get}} - form - label - | {{_ 'card-json'}} - textarea.js-card-json(placeholder="{{_ 'card-json-placeholder'}}" autofocus) - input.primary.wide(type="submit" value="{{_ 'import'}}") - template(name="boardLists") ul.pop-over-list each currentBoard.lists diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index e34d23fd0..4f5fc3a01 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -49,45 +49,6 @@ Template.listActionPopup.events({ }, }); - -BlazeComponent.extendComponent({ - events() { - return [{ - 'submit': (evt) => { - evt.preventDefault(); - const jsonData = $(evt.currentTarget).find('textarea').val(); - const firstCardDom = $(`#js-list-${this.currentData()._id} .js-minicard:first`).get(0); - const sortIndex = Utils.calculateIndex(null, firstCardDom).base; - let trelloCard; - try { - trelloCard = JSON.parse(jsonData); - } catch (e) { - this.setError('error-json-malformed'); - return; - } - Meteor.call('importTrelloCard', trelloCard, this.currentData()._id, sortIndex, - (error, response) => { - if (error) { - this.setError(error.error); - } else { - Filter.addException(response); - Popup.close(); - } - } - ); - }, - }]; - }, - - onCreated() { - this.error = new ReactiveVar(''); - }, - - setError(error) { - this.error.set(error); - }, -}).register('listImportCardPopup'); - Template.listMoveCardsPopup.events({ 'click .js-select-list'() { const fromList = Template.parentData(2).data; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index efc6128de..82ea14e5d 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -54,6 +54,7 @@ "boardChangeColorPopup-title": "Change Board Background", "boardChangeTitlePopup-title": "Rename Board", "boardChangeVisibilityPopup-title": "Change Visibility", + "boardImportBoardPopup-title": "Import board from Trello", "boardMenuPopup-title": "Board Menu", "boards": "Boards", "bucket-example": "Like “Bucket List” for example", @@ -66,8 +67,6 @@ "card-edit-attachments": "Edit attachments", "card-edit-labels": "Edit labels", "card-edit-members": "Edit members", - "card-json": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text", - "card-json-placeholder": "Paste your valid JSON data here", "card-labels-title": "Change the labels for the card.", "card-members-title": "Add or remove members of the board from the card.", "cardAttachmentsPopup-title": "Attach From", @@ -136,7 +135,11 @@ "header-logo-title": "Go back to your boards page.", "home": "Home", "import": "Import", + "import-board": "import from Trello", + "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text", "import-card": "Import a Trello card", + "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text", + "import-json-placeholder": "Paste your valid JSON data here", "info": "Infos", "initials": "Initials", "joined": "joined", @@ -175,6 +178,7 @@ "normal": "Normal", "normal-desc": "Can view and edit cards. Can't change settings.", "optional": "optional", + "or": "or", "page-maybe-private": "This page may be private. You may be able to view it by logging in.", "page-not-found": "Page not found.", "password": "Password", diff --git a/models/import.js b/models/import.js index 4fe4b4787..e81dd42a1 100644 --- a/models/import.js +++ b/models/import.js @@ -1,5 +1,5 @@ Meteor.methods({ - importTrelloCard(trelloCard, listId, sortIndex) { + importTrelloCard(trelloCard, data) { // 1. check parameters are ok from a syntax point of view const DateString = Match.Where(function (dateAsString) { check(dateAsString, String); @@ -22,14 +22,16 @@ Meteor.methods({ })], members: [Object], })); - check(listId, String); - check(sortIndex, Number); + check(data, { + listId: String, + sortIndex: Number, + }); } catch(e) { throw new Meteor.Error('error-json-schema'); } // 2. check parameters are ok from a business point of view (exist & authorized) - const list = Lists.findOne(listId); + const list = Lists.findOne(data.listId); if(!list) { throw new Meteor.Error('error-list-doesNotExist'); } @@ -49,7 +51,7 @@ Meteor.methods({ dateLastActivity: dateOfImport, description: trelloCard.desc, listId: list._id, - sort: sortIndex, + sort: data.sortIndex, title: trelloCard.name, // XXX use the original user? userId: Meteor.userId(), @@ -127,4 +129,17 @@ Meteor.methods({ }); return cardId; }, + importTrelloBoard(trelloBoard, data) { + // 1. check parameters are ok from a syntax point of view + try { + // XXX do proper checking + check(trelloBoard, Object); + check(data, Object); + } catch(e) { + throw new Meteor.Error('error-json-schema'); + } + // 2. check parameters are ok from a business point of view (exist & authorized) + // XXX check we are allowed + // 3. create all elements + }, }); From 595d5f97ac7b95ff71b391071d7d339e4ccbd4f6 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Sat, 17 Oct 2015 19:29:25 +0200 Subject: [PATCH 2/8] Import board: now proper createdAt dates --- client/components/import/import.js | 10 +++- models/import.js | 73 ++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/client/components/import/import.js b/client/components/import/import.js index f15185edb..a2972562c 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -28,7 +28,7 @@ const ImportPopup = BlazeComponent.extendComponent({ this.setError(error.error); } else { Filter.addException(response); - Popup.close(); + this.onFinish(response); } } ); @@ -43,6 +43,10 @@ const ImportPopup = BlazeComponent.extendComponent({ setError(error) { this.error.set(error); }, + + onFinish() { + Popup.close(); + } }); ImportPopup.extendComponent({ @@ -76,5 +80,9 @@ ImportPopup.extendComponent({ getLabel() { return 'import-board-trello-instruction'; }, + + onFinish(response) { + Utils.goBoardId(response); + }, }).register('boardImportBoardPopup'); diff --git a/models/import.js b/models/import.js index e81dd42a1..e0badc664 100644 --- a/models/import.js +++ b/models/import.js @@ -1,3 +1,72 @@ +const trelloCreator = { + // the object creation dates, indexed by Trello id (so we only parse actions once!) + createdAt: { + board: null, + cards: {}, + lists: {}, + }, + + // the labels we created, indexed by Trello id (to map when importing cards) + labels: {}, + + /** + * must call parseActions before calling this one + */ + createBoard(trelloBoard, dateOfImport) { + const createdAt = this.createdAt.board; + const boardToCreate = { + archived: trelloBoard.closed, + // XXX map from Trello colors + color: Boards.simpleSchema()._schema.color.allowedValues[0], + createdAt, + labels: [], + members: [{ + userId: Meteor.userId(), + isAdmin: true, + isActive: true, + }], + // XXX make a more robust mapping algorithm? + permission: trelloBoard.prefs.permissionLevel, + slug: getSlug(trelloBoard.name) || 'board', + stars: 0, + title: trelloBoard.name, + }; + trelloBoard.labels.forEach((label) => { + labelToCreate = { + _id: Random.id(6), + color: label.color, + name: label.name, + }; + // we need to remember them by Trello ID, as this is the only ref we have when importing cards + this.labels[label.id] = labelToCreate; + boardToCreate.labels.push(labelToCreate); + }); + const boardId = Boards.direct.insert(boardToCreate); + return boardId; + }, + + parseActions(trelloActions) { + trelloActions.forEach((action) =>{ + switch (action.type) { + case 'createBoard': + this.createdAt.board = action.date; + break; + case 'createCard': + const cardId = action.data.card.id; + this.createdAt.cards[cardId] = action.date; + break; + case 'createList': + const listId = action.data.list.id; + this.createdAt.lists[listId] = action.date; + break; + default: + // do nothing + break; + } + }); + } +} + Meteor.methods({ importTrelloCard(trelloCard, data) { // 1. check parameters are ok from a syntax point of view @@ -141,5 +210,9 @@ Meteor.methods({ // 2. check parameters are ok from a business point of view (exist & authorized) // XXX check we are allowed // 3. create all elements + const dateOfImport = new Date(); + trelloCreator.parseActions(trelloBoard.actions); + const boardId = trelloCreator.createBoard(trelloBoard, dateOfImport); + return boardId; }, }); From 469d47cd9f6e92518beb5f28cc7e755bf2ae1578 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Sun, 18 Oct 2015 01:02:44 +0200 Subject: [PATCH 3/8] Import board: create board, lists, and cards --- models/import.js | 79 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/models/import.js b/models/import.js index e0badc664..d0b25173e 100644 --- a/models/import.js +++ b/models/import.js @@ -1,18 +1,21 @@ -const trelloCreator = { - // the object creation dates, indexed by Trello id (so we only parse actions once!) - createdAt: { - board: null, - cards: {}, - lists: {}, - }, - - // the labels we created, indexed by Trello id (to map when importing cards) - labels: {}, +class TrelloCreator { + constructor() { + // the object creation dates, indexed by Trello id (so we only parse actions once!) + this.createdAt = { + board: null, + cards: {}, + lists: {}, + }; + // the labels we created, indexed by Trello id (to map when importing cards) + this.labels = {}; + // the lists we created, indexed by Trello id (to map when importing cards) + this.lists = {}; + } /** * must call parseActions before calling this one */ - createBoard(trelloBoard, dateOfImport) { + createBoardAndLabels(trelloBoard, dateOfImport) { const createdAt = this.createdAt.board; const boardToCreate = { archived: trelloBoard.closed, @@ -42,8 +45,51 @@ const trelloCreator = { boardToCreate.labels.push(labelToCreate); }); const boardId = Boards.direct.insert(boardToCreate); + // XXX add activities return boardId; - }, + } + + createLists(trelloLists, boardId, dateOfImport) { + trelloLists.forEach((list) => { + const listToCreate = { + archived: list.closed, + boardId, + createdAt: this.createdAt.lists[list.id], + title: list.name, + userId: Meteor.userId(), + }; + listToCreate._id = Lists.direct.insert(listToCreate); + this.lists[list.id] = listToCreate; + // XXX add activities + }); + } + + createCards(trelloCards, boardId, dateOfImport) { + trelloCards.forEach((card) => { + const cardToCreate = { + archived: card.closed, + boardId, + createdAt: this.createdAt.cards[card.id], + dateLastActivity: dateOfImport, + description: card.desc, + listId: this.lists[card.idList]._id, + sort: card.pos, + title: card.name, + // XXX use the original user? + userId: Meteor.userId(), + }; + // add labels + if(card.idLabels) { + cardToCreate.labelIds = card.idLabels.map((trelloId) => { + return this.labels[trelloId]._id; + }); + } + Cards.direct.insert(cardToCreate); + // XXX add comments + // XXX add attachments + // XXX add activities + }); + } parseActions(trelloActions) { trelloActions.forEach((action) =>{ @@ -59,6 +105,7 @@ const trelloCreator = { const listId = action.data.list.id; this.createdAt.lists[listId] = action.date; break; + // XXX extract comments as well default: // do nothing break; @@ -199,6 +246,7 @@ Meteor.methods({ return cardId; }, importTrelloBoard(trelloBoard, data) { + const trelloCreator = new TrelloCreator(); // 1. check parameters are ok from a syntax point of view try { // XXX do proper checking @@ -212,7 +260,12 @@ Meteor.methods({ // 3. create all elements const dateOfImport = new Date(); trelloCreator.parseActions(trelloBoard.actions); - const boardId = trelloCreator.createBoard(trelloBoard, dateOfImport); + const boardId = trelloCreator.createBoardAndLabels(trelloBoard, dateOfImport); + trelloCreator.createLists(trelloBoard.lists, boardId, dateOfImport); + trelloCreator.createCards(trelloBoard.cards, boardId, dateOfImport); + // XXX add activities + // XXX set modifiedAt or lastActivity + // XXX add members return boardId; }, }); From 4540bd36c4b07080ea5d29f0fb31bb20e637c2d5 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Mon, 19 Oct 2015 00:59:50 +0200 Subject: [PATCH 4/8] Import board: import comments and log activities --- client/components/activities/activities.jade | 86 +++++----- client/components/activities/activities.js | 17 +- client/components/import/import.js | 2 +- i18n/en.i18n.json | 1 + models/import.js | 166 +++++++++++++------ 5 files changed, 179 insertions(+), 93 deletions(-) diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade index c611ad75a..28a9f9c93 100644 --- a/client/components/activities/activities.jade +++ b/client/components/activities/activities.jade @@ -14,56 +14,62 @@ template(name="boardActivities") p.activity-desc +memberName(user=user) - if($eq activityType 'createBoard') - | {{_ 'activity-created' boardLabel}}. - - if($eq activityType 'createList') - | {{_ 'activity-added' list.title boardLabel}}. - - if($eq activityType 'archivedList') - | {{_ 'activity-archived' list.title}}. - - if($eq activityType 'createCard') - | {{{_ 'activity-added' cardLink boardLabel}}}. - - if($eq activityType 'importCard') - | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}. - - if($eq activityType 'archivedCard') - | {{{_ 'activity-archived' cardLink}}}. - - if($eq activityType 'restoredCard') - | {{{_ 'activity-sent' cardLink boardLabel}}}. - - if($eq activityType 'moveCard') - | {{{_ 'activity-moved' cardLink oldList.title list.title}}}. + if($eq activityType 'addAttachment') + | {{{_ 'activity-attached' attachmentLink cardLink}}}. if($eq activityType 'addBoardMember') | {{{_ 'activity-added' memberLink boardLabel}}}. - if($eq activityType 'removeBoardMember') - | {{{_ 'activity-excluded' memberLink boardLabel}}}. - - if($eq activityType 'joinMember') - if($eq currentUser._id member._id) - | {{{_ 'activity-joined' cardLink}}}. - else - | {{{_ 'activity-added' memberLink cardLink}}}. - - if($eq activityType 'unjoinMember') - if($eq currentUser._id member._id) - | {{{_ 'activity-unjoined' cardLink}}}. - else - | {{{_ 'activity-removed' memberLink cardLink}}}. - if($eq activityType 'addComment') | {{{_ 'activity-on' cardLink}}} a.activity-comment(href="{{ card.absoluteUrl }}") +viewer = comment.text - if($eq activityType 'addAttachment') - | {{{_ 'activity-attached' attachmentLink cardLink}}}. + if($eq activityType 'archivedCard') + | {{{_ 'activity-archived' cardLink}}}. + + if($eq activityType 'archivedList') + | {{_ 'activity-archived' list.title}}. + + if($eq activityType 'createBoard') + | {{_ 'activity-created' boardLabel}}. + + if($eq activityType 'createCard') + | {{{_ 'activity-added' cardLink boardLabel}}}. + + if($eq activityType 'createList') + | {{_ 'activity-added' list.title boardLabel}}. + + if($eq activityType 'importBoard') + | {{{_ 'activity-imported-board' boardLabel sourceLink}}}. + + if($eq activityType 'importCard') + | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}. + + if($eq activityType 'importList') + | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}. + + if($eq activityType 'joinMember') + if($eq currentUser._id member._id) + | {{{_ 'activity-joined' cardLink}}}. + else + | {{{_ 'activity-added' memberLink cardLink}}}. + + if($eq activityType 'moveCard') + | {{{_ 'activity-moved' cardLink oldList.title list.title}}}. + + if($eq activityType 'removeBoardMember') + | {{{_ 'activity-excluded' memberLink boardLabel}}}. + + if($eq activityType 'restoredCard') + | {{{_ 'activity-sent' cardLink boardLabel}}}. + + if($eq activityType 'unjoinMember') + if($eq currentUser._id member._id) + | {{{_ 'activity-unjoined' cardLink}}}. + else + | {{{_ 'activity-removed' memberLink cardLink}}}. span.activity-meta {{ moment createdAt }} diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index b80493f74..b25c0ca82 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -60,11 +60,22 @@ BlazeComponent.extendComponent({ }, card.title)); }, + listLabel() { + return this.currentData().list().title; + }, + sourceLink() { const source = this.currentData().source; - return source && Blaze.toHTML(HTML.A({ - href: source.url, - }, source.system)); + if(source) { + if(source.url) { + return Blaze.toHTML(HTML.A({ + href: source.url, + }, source.system)); + } else { + return source.system; + } + } + return null; }, memberLink() { diff --git a/client/components/import/import.js b/client/components/import/import.js index a2972562c..00918aac5 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -46,7 +46,7 @@ const ImportPopup = BlazeComponent.extendComponent({ onFinish() { Popup.close(); - } + }, }); ImportPopup.extendComponent({ diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 82ea14e5d..0823ba085 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -8,6 +8,7 @@ "activity-created": "created %s", "activity-excluded": "excluded %s from %s", "activity-imported": "imported %s into %s from %s", + "activity-imported-board": "imported %s from %s", "activity-joined": "joined %s", "activity-moved": "moved %s from %s to %s", "activity-on": "on %s", diff --git a/models/import.js b/models/import.js index d0b25173e..5095ee2eb 100644 --- a/models/import.js +++ b/models/import.js @@ -10,12 +10,14 @@ class TrelloCreator { this.labels = {}; // the lists we created, indexed by Trello id (to map when importing cards) this.lists = {}; + // the comments, indexed by Trello card id (to map when importing cards) + this.comments = {}; } /** * must call parseActions before calling this one */ - createBoardAndLabels(trelloBoard, dateOfImport) { + createBoardAndLabels(trelloBoard) { const createdAt = this.createdAt.board; const boardToCreate = { archived: trelloBoard.closed, @@ -35,7 +37,7 @@ class TrelloCreator { title: trelloBoard.name, }; trelloBoard.labels.forEach((label) => { - labelToCreate = { + const labelToCreate = { _id: Random.id(6), color: label.color, name: label.name, @@ -45,11 +47,23 @@ class TrelloCreator { boardToCreate.labels.push(labelToCreate); }); const boardId = Boards.direct.insert(boardToCreate); - // XXX add activities + // log activity + Activities.direct.insert({ + activityType: 'importBoard', + boardId, + createdAt: new Date(), + source: { + id: trelloBoard.id, + system: 'Trello', + url: trelloBoard.url, + }, + // we attribute the import to current user, not the one from the original object + userId: Meteor.userId(), + }); return boardId; } - createLists(trelloLists, boardId, dateOfImport) { + createLists(trelloLists, boardId) { trelloLists.forEach((list) => { const listToCreate = { archived: list.closed, @@ -60,17 +74,29 @@ class TrelloCreator { }; listToCreate._id = Lists.direct.insert(listToCreate); this.lists[list.id] = listToCreate; - // XXX add activities + // log activity + Activities.direct.insert({ + activityType: 'importList', + boardId, + createdAt: new Date(), + listId: listToCreate._id, + source: { + id: list.id, + system: 'Trello', + }, + // we attribute the import to current user, not the one from the original object + userId: Meteor.userId(), + }); }); } - createCards(trelloCards, boardId, dateOfImport) { + createCardsAndComments(trelloCards, boardId) { trelloCards.forEach((card) => { const cardToCreate = { archived: card.closed, boardId, createdAt: this.createdAt.cards[card.id], - dateLastActivity: dateOfImport, + dateLastActivity: new Date(), description: card.desc, listId: this.lists[card.idList]._id, sort: card.pos, @@ -84,37 +110,102 @@ class TrelloCreator { return this.labels[trelloId]._id; }); } - Cards.direct.insert(cardToCreate); - // XXX add comments + // insert card + const cardId = Cards.direct.insert(cardToCreate); + // log activity + Activities.direct.insert({ + activityType: 'importCard', + boardId, + cardId, + createdAt: new Date(), + listId: cardToCreate.listId, + source: { + id: card.id, + system: 'Trello', + url: card.url, + }, + // we attribute the import to current user, not the one from the original card + userId: Meteor.userId(), + }); + // add comments + const comments = this.comments[card.id]; + if(comments) { + comments.forEach((comment) => { + const commentToCreate = { + boardId, + cardId, + createdAt: comment.date, + text: comment.data.text, + // XXX use the original comment user instead + userId: Meteor.userId(), + }; + const commentId = CardComments.direct.insert(commentToCreate); + Activities.direct.insert({ + activityType: 'addComment', + boardId: commentToCreate.boardId, + cardId: commentToCreate.cardId, + commentId, + createdAt: commentToCreate.createdAt, + userId: commentToCreate.userId, + }); + }); + } // XXX add attachments - // XXX add activities }); } parseActions(trelloActions) { - trelloActions.forEach((action) =>{ + trelloActions.forEach((action) => { switch (action.type) { - case 'createBoard': - this.createdAt.board = action.date; - break; - case 'createCard': - const cardId = action.data.card.id; - this.createdAt.cards[cardId] = action.date; - break; - case 'createList': - const listId = action.data.list.id; - this.createdAt.lists[listId] = action.date; - break; - // XXX extract comments as well - default: - // do nothing - break; + case 'createBoard': + this.createdAt.board = action.date; + break; + case 'createCard': + const cardId = action.data.card.id; + this.createdAt.cards[cardId] = action.date; + break; + case 'createList': + const listId = action.data.list.id; + this.createdAt.lists[listId] = action.date; + break; + case 'commentCard': + const id = action.data.card.id; + if(this.comments[id]) { + this.comments[id].push(action); + } else { + this.comments[id] = [action]; + } + break; + default: + // do nothing + break; } }); } } Meteor.methods({ + importTrelloBoard(trelloBoard, data) { + const trelloCreator = new TrelloCreator(); + // 1. check parameters are ok from a syntax point of view + try { + // XXX do proper checking + check(trelloBoard, Object); + check(data, Object); + } catch(e) { + throw new Meteor.Error('error-json-schema'); + } + // 2. check parameters are ok from a business point of view (exist & authorized) + // XXX check we are allowed + // 3. create all elements + trelloCreator.parseActions(trelloBoard.actions); + const boardId = trelloCreator.createBoardAndLabels(trelloBoard); + trelloCreator.createLists(trelloBoard.lists, boardId); + trelloCreator.createCardsAndComments(trelloBoard.cards, boardId); + // XXX set modifiedAt or lastActivity + // XXX add members + return boardId; + }, importTrelloCard(trelloCard, data) { // 1. check parameters are ok from a syntax point of view const DateString = Match.Where(function (dateAsString) { @@ -245,27 +336,4 @@ Meteor.methods({ }); return cardId; }, - importTrelloBoard(trelloBoard, data) { - const trelloCreator = new TrelloCreator(); - // 1. check parameters are ok from a syntax point of view - try { - // XXX do proper checking - check(trelloBoard, Object); - check(data, Object); - } catch(e) { - throw new Meteor.Error('error-json-schema'); - } - // 2. check parameters are ok from a business point of view (exist & authorized) - // XXX check we are allowed - // 3. create all elements - const dateOfImport = new Date(); - trelloCreator.parseActions(trelloBoard.actions); - const boardId = trelloCreator.createBoardAndLabels(trelloBoard, dateOfImport); - trelloCreator.createLists(trelloBoard.lists, boardId, dateOfImport); - trelloCreator.createCards(trelloBoard.cards, boardId, dateOfImport); - // XXX add activities - // XXX set modifiedAt or lastActivity - // XXX add members - return boardId; - }, }); From 456674f1114f6e698891ad3b5b8b4bd505a550c5 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Mon, 19 Oct 2015 11:46:04 +0200 Subject: [PATCH 5/8] Import board: set proper color and modifiedAt dates --- models/import.js | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/models/import.js b/models/import.js index 5095ee2eb..e925d0287 100644 --- a/models/import.js +++ b/models/import.js @@ -21,8 +21,7 @@ class TrelloCreator { const createdAt = this.createdAt.board; const boardToCreate = { archived: trelloBoard.closed, - // XXX map from Trello colors - color: Boards.simpleSchema()._schema.color.allowedValues[0], + color: this.getColor(trelloBoard.prefs.background), createdAt, labels: [], members: [{ @@ -46,12 +45,14 @@ class TrelloCreator { this.labels[label.id] = labelToCreate; boardToCreate.labels.push(labelToCreate); }); + const now = new Date(); const boardId = Boards.direct.insert(boardToCreate); + Boards.direct.update(boardId, {$set: {modifiedAt: now}}); // log activity Activities.direct.insert({ activityType: 'importBoard', boardId, - createdAt: new Date(), + createdAt: now, source: { id: trelloBoard.id, system: 'Trello', @@ -72,14 +73,17 @@ class TrelloCreator { title: list.name, userId: Meteor.userId(), }; - listToCreate._id = Lists.direct.insert(listToCreate); + const listId = Lists.direct.insert(listToCreate); + const now = new Date(); + Lists.direct.update(listId, {$set: {'updatedAt': now}}); + listToCreate._id = listId; this.lists[list.id] = listToCreate; // log activity Activities.direct.insert({ activityType: 'importList', boardId, - createdAt: new Date(), - listId: listToCreate._id, + createdAt: now, + listId, source: { id: list.id, system: 'Trello', @@ -139,6 +143,7 @@ class TrelloCreator { // XXX use the original comment user instead userId: Meteor.userId(), }; + // dateLastActivity will be set from activity insert, no need to update it ourselves const commentId = CardComments.direct.insert(commentToCreate); Activities.direct.insert({ activityType: 'addComment', @@ -154,6 +159,23 @@ class TrelloCreator { }); } + getColor(trelloColorCode) { + // trello color name => wekan color + const mapColors = { + 'blue': 'belize', + 'orange': 'pumpkin', + 'green': 'nephritis', + 'red': 'pomegranate', + 'purple': 'wisteria', + 'pink': 'pomegranate', + 'lime': 'nephritis', + 'sky': 'belize', + 'grey': 'midnight', + }; + const wekanColor = mapColors[trelloColorCode]; + return wekanColor || Boards.simpleSchema()._schema.color.allowedValues[0]; + } + parseActions(trelloActions) { trelloActions.forEach((action) => { switch (action.type) { @@ -202,7 +224,6 @@ Meteor.methods({ const boardId = trelloCreator.createBoardAndLabels(trelloBoard); trelloCreator.createLists(trelloBoard.lists, boardId); trelloCreator.createCardsAndComments(trelloBoard.cards, boardId); - // XXX set modifiedAt or lastActivity // XXX add members return boardId; }, From ec304de811d41f2679fc8ef171c0884db8bc9014 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Mon, 19 Oct 2015 12:41:56 +0200 Subject: [PATCH 6/8] Import board: check json structure before importing --- models/import.js | 64 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/models/import.js b/models/import.js index e925d0287..742fcf2fc 100644 --- a/models/import.js +++ b/models/import.js @@ -1,3 +1,8 @@ +const DateString = Match.Where(function (dateAsString) { + check(dateAsString, String); + return moment(dateAsString, moment.ISO_8601).isValid(); +}); + class TrelloCreator { constructor() { // the object creation dates, indexed by Trello id (so we only parse actions once!) @@ -14,6 +19,50 @@ class TrelloCreator { this.comments = {}; } + checkActions(trelloActions) { + check(trelloActions, [Match.ObjectIncluding({ + data: Object, + date: DateString, + type: String, + })]); + // XXX perform deeper checks based on type + } + + checkBoard(trelloBoard) { + check(trelloBoard, Match.ObjectIncluding({ + closed: Boolean, + labels: [Match.ObjectIncluding({ + // XXX check versus list + color: String, + name: String, + })], + name: String, + prefs: Match.ObjectIncluding({ + // XXX check versus list + background: String, + // XXX check versus list + permissionLevel: String, + }), + })); + } + + checkLists(trelloLists) { + check(trelloLists, [Match.ObjectIncluding({ + closed: Boolean, + name: String, + })]); + } + + checkCards(trelloCards) { + check(trelloCards, [Match.ObjectIncluding({ + closed: Boolean, + desc: String, + // XXX check idLabels + name: String, + pos: Number, + })]); + } + /** * must call parseActions before calling this one */ @@ -29,7 +78,7 @@ class TrelloCreator { isAdmin: true, isActive: true, }], - // XXX make a more robust mapping algorithm? + // current mapping is easy as trello and wekan use same keys: 'private' and 'public' permission: trelloBoard.prefs.permissionLevel, slug: getSlug(trelloBoard.name) || 'board', stars: 0, @@ -209,16 +258,19 @@ class TrelloCreator { Meteor.methods({ importTrelloBoard(trelloBoard, data) { const trelloCreator = new TrelloCreator(); - // 1. check parameters are ok from a syntax point of view + // 1. check all parameters are ok from a syntax point of view try { - // XXX do proper checking - check(trelloBoard, Object); - check(data, Object); + // we don't use additional data - this should be an empty object + check(data, {}); + trelloCreator.checkActions(trelloBoard.actions); + trelloCreator.checkBoard(trelloBoard); + trelloCreator.checkLists(trelloBoard.lists); + trelloCreator.checkCards(trelloBoard.cards); } catch(e) { throw new Meteor.Error('error-json-schema'); } // 2. check parameters are ok from a business point of view (exist & authorized) - // XXX check we are allowed + // nothing to check, everyone can import boards in their account // 3. create all elements trelloCreator.parseActions(trelloBoard.actions); const boardId = trelloCreator.createBoardAndLabels(trelloBoard); From 8e0ad9119190ac0cfa22827fa278b498eba02d6c Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Mon, 19 Oct 2015 20:14:29 +0200 Subject: [PATCH 7/8] Import board: map team permission, and refactor code to share with card import --- models/boards.js | 8 ++ models/import.js | 210 +++++++++++++++++------------------------------ 2 files changed, 83 insertions(+), 135 deletions(-) diff --git a/models/boards.js b/models/boards.js index fd0212c59..e42e06c62 100644 --- a/models/boards.js +++ b/models/boards.js @@ -111,6 +111,14 @@ Boards.helpers({ colorClass() { return `board-color-${this.color}`; }, + + // XXX currently mutations return no value so we have an issue when using addLabel in import + // XXX waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove... + pushLabel(name, color) { + const _id = Random.id(6); + Boards.direct.update(this._id, { $push: {labels: { _id, name, color }}}); + return _id; + }, }); Boards.mutations({ diff --git a/models/import.js b/models/import.js index 742fcf2fc..be6997460 100644 --- a/models/import.js +++ b/models/import.js @@ -11,9 +11,9 @@ class TrelloCreator { cards: {}, lists: {}, }; - // the labels we created, indexed by Trello id (to map when importing cards) + // map of labels Trello ID => Wekan ID this.labels = {}; - // the lists we created, indexed by Trello id (to map when importing cards) + // map of lists Trello ID => Wekan ID this.lists = {}; // the comments, indexed by Trello card id (to map when importing cards) this.comments = {}; @@ -25,27 +25,41 @@ class TrelloCreator { date: DateString, type: String, })]); - // XXX perform deeper checks based on type + // XXX we could perform more thorough checks based on action type } checkBoard(trelloBoard) { check(trelloBoard, Match.ObjectIncluding({ closed: Boolean, - labels: [Match.ObjectIncluding({ - // XXX check versus list - color: String, - name: String, - })], name: String, prefs: Match.ObjectIncluding({ - // XXX check versus list + // XXX refine control by validating 'background' against a list of allowed values (is it worth the maintenance?) background: String, - // XXX check versus list - permissionLevel: String, + permissionLevel: Match.Where((value) => {return ['org', 'private', 'public'].indexOf(value)>= 0;}), }), })); } + checkCards(trelloCards) { + check(trelloCards, [Match.ObjectIncluding({ + closed: Boolean, + dateLastActivity: DateString, + desc: String, + idLabels: [String], + idMembers: [String], + name: String, + pos: Number, + })]); + } + + checkLabels(trelloLabels) { + check(trelloLabels, [Match.ObjectIncluding({ + // XXX refine control by validating 'color' against a list of allowed values (is it worth the maintenance?) + color: String, + name: String, + })]); + } + checkLists(trelloLists) { check(trelloLists, [Match.ObjectIncluding({ closed: Boolean, @@ -53,16 +67,6 @@ class TrelloCreator { })]); } - checkCards(trelloCards) { - check(trelloCards, [Match.ObjectIncluding({ - closed: Boolean, - desc: String, - // XXX check idLabels - name: String, - pos: Number, - })]); - } - /** * must call parseActions before calling this one */ @@ -78,8 +82,7 @@ class TrelloCreator { isAdmin: true, isActive: true, }], - // current mapping is easy as trello and wekan use same keys: 'private' and 'public' - permission: trelloBoard.prefs.permissionLevel, + permission: this.getPermission(trelloBoard.prefs.permissionLevel), slug: getSlug(trelloBoard.name) || 'board', stars: 0, title: trelloBoard.name, @@ -91,7 +94,7 @@ class TrelloCreator { name: label.name, }; // we need to remember them by Trello ID, as this is the only ref we have when importing cards - this.labels[label.id] = labelToCreate; + this.labels[label.id] = labelToCreate._id; boardToCreate.labels.push(labelToCreate); }); const now = new Date(); @@ -113,6 +116,23 @@ class TrelloCreator { return boardId; } + /** + * Create labels if they do not exist and load this.labels. + */ + createLabels(trelloLabels, board) { + trelloLabels.forEach((label) => { + const color = label.color; + const name = label.name; + const existingLabel = board.getLabel(name, color); + if (existingLabel) { + this.labels[label.id] = existingLabel._id; + } else { + const idLabelCreated = board.pushLabel(name, color); + this.labels[label.id] = idLabelCreated; + } + }); + } + createLists(trelloLists, boardId) { trelloLists.forEach((list) => { const listToCreate = { @@ -125,8 +145,7 @@ class TrelloCreator { const listId = Lists.direct.insert(listToCreate); const now = new Date(); Lists.direct.update(listId, {$set: {'updatedAt': now}}); - listToCreate._id = listId; - this.lists[list.id] = listToCreate; + this.lists[list.id] = listId; // log activity Activities.direct.insert({ activityType: 'importList', @@ -144,6 +163,7 @@ class TrelloCreator { } createCardsAndComments(trelloCards, boardId) { + const result = []; trelloCards.forEach((card) => { const cardToCreate = { archived: card.closed, @@ -151,7 +171,7 @@ class TrelloCreator { createdAt: this.createdAt.cards[card.id], dateLastActivity: new Date(), description: card.desc, - listId: this.lists[card.idList]._id, + listId: this.lists[card.idList], sort: card.pos, title: card.name, // XXX use the original user? @@ -160,7 +180,7 @@ class TrelloCreator { // add labels if(card.idLabels) { cardToCreate.labelIds = card.idLabels.map((trelloId) => { - return this.labels[trelloId]._id; + return this.labels[trelloId]; }); } // insert card @@ -205,7 +225,9 @@ class TrelloCreator { }); } // XXX add attachments + result.push(cardId); }); + return result; } getColor(trelloColorCode) { @@ -225,6 +247,14 @@ class TrelloCreator { return wekanColor || Boards.simpleSchema()._schema.color.allowedValues[0]; } + getPermission(trelloPermissionCode) { + if(trelloPermissionCode === 'public') { + return 'public'; + } + // Wekan does NOT have organization level, so we default both 'private' and 'org' to private. + return 'private'; + } + parseActions(trelloActions) { trelloActions.forEach((action) => { switch (action.type) { @@ -258,19 +288,23 @@ class TrelloCreator { Meteor.methods({ importTrelloBoard(trelloBoard, data) { const trelloCreator = new TrelloCreator(); + // 1. check all parameters are ok from a syntax point of view try { // we don't use additional data - this should be an empty object check(data, {}); trelloCreator.checkActions(trelloBoard.actions); trelloCreator.checkBoard(trelloBoard); + trelloCreator.checkLabels(trelloBoard.labels); trelloCreator.checkLists(trelloBoard.lists); trelloCreator.checkCards(trelloBoard.cards); } catch(e) { throw new Meteor.Error('error-json-schema'); } + // 2. check parameters are ok from a business point of view (exist & authorized) // nothing to check, everyone can import boards in their account + // 3. create all elements trelloCreator.parseActions(trelloBoard.actions); const boardId = trelloCreator.createBoardAndLabels(trelloBoard); @@ -279,33 +313,19 @@ Meteor.methods({ // XXX add members return boardId; }, + importTrelloCard(trelloCard, data) { + const trelloCreator = new TrelloCreator(); + // 1. check parameters are ok from a syntax point of view - const DateString = Match.Where(function (dateAsString) { - check(dateAsString, String); - return moment(dateAsString, moment.ISO_8601).isValid(); - }); try { - check(trelloCard, Match.ObjectIncluding({ - name: String, - desc: String, - closed: Boolean, - dateLastActivity: DateString, - labels: [Match.ObjectIncluding({ - name: String, - color: String, - })], - actions: [Match.ObjectIncluding({ - type: String, - date: DateString, - data: Object, - })], - members: [Object], - })); check(data, { listId: String, sortIndex: Number, }); + trelloCreator.checkCards([trelloCard]); + trelloCreator.checkLabels(trelloCard.labels); + trelloCreator.checkActions(trelloCard.actions); } catch(e) { throw new Meteor.Error('error-json-schema'); } @@ -321,92 +341,12 @@ Meteor.methods({ } } - // 3. map all fields for the card to create - const dateOfImport = new Date(); - const cardToCreate = { - archived: trelloCard.closed, - boardId: list.boardId, - // this is a default date, we'll fetch the actual one from the actions array - createdAt: dateOfImport, - dateLastActivity: dateOfImport, - description: trelloCard.desc, - listId: list._id, - sort: data.sortIndex, - title: trelloCard.name, - // XXX use the original user? - userId: Meteor.userId(), - }; - - // 4. find actual creation date - const creationAction = trelloCard.actions.find((action) => { - return action.type === 'createCard'; - }); - if(creationAction) { - cardToCreate.createdAt = creationAction.date; - } - - // 5. map labels - create missing ones - trelloCard.labels.forEach((currentLabel) => { - const color = currentLabel.color; - const name = currentLabel.name; - const existingLabel = list.board().getLabel(name, color); - let labelId = undefined; - if (existingLabel) { - labelId = existingLabel._id; - } else { - let labelCreated = list.board().addLabel(name, color); - // XXX currently mutations return no value so we have to fetch the label we just created - // waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove... - labelCreated = list.board().getLabel(name, color); - labelId = labelCreated._id; - } - if(labelId) { - if (!cardToCreate.labelIds) { - cardToCreate.labelIds = []; - } - cardToCreate.labelIds.push(labelId); - } - }); - - // 6. insert new card into list - const cardId = Cards.direct.insert(cardToCreate); - Activities.direct.insert({ - activityType: 'importCard', - boardId: cardToCreate.boardId, - cardId, - createdAt: dateOfImport, - listId: cardToCreate.listId, - source: { - id: trelloCard.id, - system: 'Trello', - url: trelloCard.url, - }, - // we attribute the import to current user, not the one from the original card - userId: Meteor.userId(), - }); - - // 7. parse actions and add comments - trelloCard.actions.forEach((currentAction) => { - if(currentAction.type === 'commentCard') { - const commentToCreate = { - boardId: list.boardId, - cardId, - createdAt: currentAction.date, - text: currentAction.data.text, - // XXX use the original comment user instead - userId: Meteor.userId(), - }; - const commentId = CardComments.direct.insert(commentToCreate); - Activities.direct.insert({ - activityType: 'addComment', - boardId: commentToCreate.boardId, - cardId: commentToCreate.cardId, - commentId, - createdAt: commentToCreate.createdAt, - userId: commentToCreate.userId, - }); - } - }); - return cardId; + // 3. create all elements + trelloCreator.lists[trelloCard.idList] = data.listId; + trelloCreator.parseActions(trelloCard.actions); + const board = list.board(); + trelloCreator.createLabels(trelloCard.labels, board); + const cardIds = trelloCreator.createCardsAndComments([trelloCard], board._id); + return cardIds[0]; }, }); From 118b434a5aad35df8eefea85624ab9abafab56f0 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 20 Oct 2015 20:02:12 +0200 Subject: [PATCH 8/8] Provide a default date for lists and cards creation date See https://github.com/wekan/wekan/pull/362#issuecomment-149645497 for motivation. This commit also contains cosmetic changes to the import Popup and on the code style to be more consistent with the code base. --- client/components/boards/boardHeader.jade | 5 +- client/components/import/import.jade | 5 +- client/components/import/import.js | 28 ++++++----- client/components/main/popup.styl | 6 +-- models/import.js | 60 ++++++++++++++--------- 5 files changed, 59 insertions(+), 45 deletions(-) diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index e460170b9..cb86e9bbe 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -107,8 +107,9 @@ template(name="createBoardPopup") | {{{_ 'board-private-info'}}} a.js-change-visibility {{_ 'change'}}. input.primary.wide(type="submit" value="{{_ 'create'}}") - | {{_ 'or'}} - a.js-import {{_ 'import-board'}} + span.quiet + | {{_ 'or'}} + a.js-import {{_ 'import-board'}} template(name="boardChangeTitlePopup") diff --git a/client/components/import/import.jade b/client/components/import/import.jade index 8059b65b3..f63661afd 100644 --- a/client/components/import/import.jade +++ b/client/components/import/import.jade @@ -2,7 +2,6 @@ template(name="importPopup") if error.get .warning {{_ error.get}} form - label - | {{_ getLabel}} - textarea.js-card-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus) + p: label(for='import-textarea') {{_ getLabel}} + textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus) input.primary.wide(type="submit" value="{{_ 'import'}}") diff --git a/client/components/import/import.js b/client/components/import/import.js index 00918aac5..c6957fa97 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -1,20 +1,21 @@ -/** - * Abstract root for all import popup screens. - * Descendants must define: - * - getMethodName(): return the Meteor method to call for import, passing json data decoded as object - * and additional data (see below) - * - getAdditionalData(): return object containing additional data passed to Meteor method - * (like list ID and position for a card import) - * - getLabel(): i18n key for the text displayed in the popup, usually to explain how to get the data out of the - * source system. - */ +/// Abstract root for all import popup screens. +/// Descendants must define: +/// - getMethodName(): return the Meteor method to call for import, passing json +/// data decoded as object and additional data (see below); +/// - getAdditionalData(): return object containing additional data passed to +/// Meteor method (like list ID and position for a card import); +/// - getLabel(): i18n key for the text displayed in the popup, usually to +/// explain how to get the data out of the source system. const ImportPopup = BlazeComponent.extendComponent({ - template() {return 'importPopup';}, + template() { + return 'importPopup'; + }, + events() { return [{ 'submit': (evt) => { evt.preventDefault(); - const dataJson = $(evt.currentTarget).find('textarea').val(); + const dataJson = $(evt.currentTarget).find('.js-import-json').val(); let dataObject; try { dataObject = JSON.parse(dataJson); @@ -52,7 +53,8 @@ const ImportPopup = BlazeComponent.extendComponent({ ImportPopup.extendComponent({ getAdditionalData() { const listId = this.data()._id; - const firstCardDom = $(`#js-list-${this.currentData()._id} .js-minicard:first`).get(0); + const selector = `#js-list-${this.currentData()._id} .js-minicard:first`; + const firstCardDom = $(selector).get(0); const sortIndex = Utils.calculateIndex(null, firstCardDom).base; const result = {listId, sortIndex}; return result; diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl index 3bef4f7d9..8a685069a 100644 --- a/client/components/main/popup.styl +++ b/client/components/main/popup.styl @@ -17,9 +17,11 @@ $popupWidth = 300px margin: 4px -10px width: $popupWidth + p, + textarea, input[type="text"], input[type="email"], - input[type="password"] + input[type="password"], input[type="file"] margin: 4px 0 12px width: 100% @@ -30,8 +32,6 @@ $popupWidth = 300px textarea height: 72px - margin: 4px 0 12px - width: 100% .header height: 36px diff --git a/models/import.js b/models/import.js index be6997460..7b441df67 100644 --- a/models/import.js +++ b/models/import.js @@ -5,17 +5,18 @@ const DateString = Match.Where(function (dateAsString) { class TrelloCreator { constructor() { - // the object creation dates, indexed by Trello id (so we only parse actions once!) + // The object creation dates, indexed by Trello id (so we only parse actions + // once!) this.createdAt = { board: null, cards: {}, lists: {}, }; - // map of labels Trello ID => Wekan ID + // Map of labels Trello ID => Wekan ID this.labels = {}; - // map of lists Trello ID => Wekan ID + // Map of lists Trello ID => Wekan ID this.lists = {}; - // the comments, indexed by Trello card id (to map when importing cards) + // The comments, indexed by Trello card id (to map when importing cards) this.comments = {}; } @@ -33,9 +34,12 @@ class TrelloCreator { closed: Boolean, name: String, prefs: Match.ObjectIncluding({ - // XXX refine control by validating 'background' against a list of allowed values (is it worth the maintenance?) + // XXX refine control by validating 'background' against a list of + // allowed values (is it worth the maintenance?) background: String, - permissionLevel: Match.Where((value) => {return ['org', 'private', 'public'].indexOf(value)>= 0;}), + permissionLevel: Match.Where((value) => { + return ['org', 'private', 'public'].indexOf(value)>= 0; + }), }), })); } @@ -54,7 +58,8 @@ class TrelloCreator { checkLabels(trelloLabels) { check(trelloLabels, [Match.ObjectIncluding({ - // XXX refine control by validating 'color' against a list of allowed values (is it worth the maintenance?) + // XXX refine control by validating 'color' against a list of allowed + // values (is it worth the maintenance?) color: String, name: String, })]); @@ -67,9 +72,7 @@ class TrelloCreator { })]); } - /** - * must call parseActions before calling this one - */ + // You must call parseActions before calling this one. createBoardAndLabels(trelloBoard) { const createdAt = this.createdAt.board; const boardToCreate = { @@ -93,7 +96,8 @@ class TrelloCreator { color: label.color, name: label.name, }; - // we need to remember them by Trello ID, as this is the only ref we have when importing cards + // We need to remember them by Trello ID, as this is the only ref we have + // when importing cards. this.labels[label.id] = labelToCreate._id; boardToCreate.labels.push(labelToCreate); }); @@ -110,15 +114,14 @@ class TrelloCreator { system: 'Trello', url: trelloBoard.url, }, - // we attribute the import to current user, not the one from the original object + // We attribute the import to current user, not the one from the original + // object. userId: Meteor.userId(), }); return boardId; } - /** - * Create labels if they do not exist and load this.labels. - */ + // Create labels if they do not exist and load this.labels. createLabels(trelloLabels, board) { trelloLabels.forEach((label) => { const color = label.color; @@ -138,7 +141,11 @@ class TrelloCreator { const listToCreate = { archived: list.closed, boardId, - createdAt: this.createdAt.lists[list.id], + // We are being defensing here by providing a default date (now) if the + // creation date wasn't found on the action log. This happen on old + // Trello boards (eg from 2013) that didn't log the 'createList' action + // we require. + createdAt: new Date(this.createdAt.lists[list.id] || Date.now()), title: list.name, userId: Meteor.userId(), }; @@ -156,7 +163,8 @@ class TrelloCreator { id: list.id, system: 'Trello', }, - // we attribute the import to current user, not the one from the original object + // We attribute the import to current user, not the one from the + // original object userId: Meteor.userId(), }); }); @@ -168,7 +176,7 @@ class TrelloCreator { const cardToCreate = { archived: card.closed, boardId, - createdAt: this.createdAt.cards[card.id], + createdAt: new Date(this.createdAt.cards[card.id] || Date.now()), dateLastActivity: new Date(), description: card.desc, listId: this.lists[card.idList], @@ -197,7 +205,8 @@ class TrelloCreator { system: 'Trello', url: card.url, }, - // we attribute the import to current user, not the one from the original card + // we attribute the import to current user, not the one from the + // original card userId: Meteor.userId(), }); // add comments @@ -212,7 +221,8 @@ class TrelloCreator { // XXX use the original comment user instead userId: Meteor.userId(), }; - // dateLastActivity will be set from activity insert, no need to update it ourselves + // dateLastActivity will be set from activity insert, no need to + // update it ourselves const commentId = CardComments.direct.insert(commentToCreate); Activities.direct.insert({ activityType: 'addComment', @@ -251,7 +261,8 @@ class TrelloCreator { if(trelloPermissionCode === 'public') { return 'public'; } - // Wekan does NOT have organization level, so we default both 'private' and 'org' to private. + // Wekan does NOT have organization level, so we default both 'private' and + // 'org' to private. return 'private'; } @@ -302,8 +313,8 @@ Meteor.methods({ throw new Meteor.Error('error-json-schema'); } - // 2. check parameters are ok from a business point of view (exist & authorized) - // nothing to check, everyone can import boards in their account + // 2. check parameters are ok from a business point of view (exist & + // authorized) nothing to check, everyone can import boards in their account // 3. create all elements trelloCreator.parseActions(trelloBoard.actions); @@ -330,7 +341,8 @@ Meteor.methods({ throw new Meteor.Error('error-json-schema'); } - // 2. check parameters are ok from a business point of view (exist & authorized) + // 2. check parameters are ok from a business point of view (exist & + // authorized) const list = Lists.findOne(data.listId); if(!list) { throw new Meteor.Error('error-list-doesNotExist');