mirror of
https://github.com/wekan/wekan.git
synced 2025-04-24 14:08:31 -04:00
Merge GitHub PR #401
I also completed the release notes related to the import feature. Closes #401
This commit is contained in:
commit
f565aed259
10 changed files with 469 additions and 117 deletions
|
@ -12,6 +12,7 @@ babel-runtime@0.1.4
|
||||||
base64@1.0.4
|
base64@1.0.4
|
||||||
binary-heap@1.0.4
|
binary-heap@1.0.4
|
||||||
blaze@2.1.3
|
blaze@2.1.3
|
||||||
|
blaze-html-templates@1.0.1
|
||||||
blaze-tools@1.0.4
|
blaze-tools@1.0.4
|
||||||
boilerplate-generator@1.0.4
|
boilerplate-generator@1.0.4
|
||||||
caching-compiler@1.0.0
|
caching-compiler@1.0.0
|
||||||
|
@ -63,7 +64,7 @@ idmontie:migrations@1.0.1
|
||||||
jquery@1.11.4
|
jquery@1.11.4
|
||||||
kadira:blaze-layout@2.2.0
|
kadira:blaze-layout@2.2.0
|
||||||
kadira:dochead@1.3.2
|
kadira:dochead@1.3.2
|
||||||
kadira:flow-router@2.8.0
|
kadira:flow-router@2.9.0
|
||||||
kenton:accounts-sandstorm@0.1.8
|
kenton:accounts-sandstorm@0.1.8
|
||||||
launch-screen@1.0.4
|
launch-screen@1.0.4
|
||||||
livedata@1.0.15
|
livedata@1.0.15
|
||||||
|
@ -124,7 +125,7 @@ seriousm:emoji-continued@1.4.0
|
||||||
service-configuration@1.0.5
|
service-configuration@1.0.5
|
||||||
session@1.1.1
|
session@1.1.1
|
||||||
sha@1.0.4
|
sha@1.0.4
|
||||||
softwarerero:accounts-t9n@1.1.4
|
softwarerero:accounts-t9n@1.1.6
|
||||||
spacebars@1.0.7
|
spacebars@1.0.7
|
||||||
spacebars-compiler@1.0.7
|
spacebars-compiler@1.0.7
|
||||||
srp@1.0.4
|
srp@1.0.4
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
This release features:
|
This release features:
|
||||||
|
|
||||||
* Card import from Trello
|
* Trello boards and cards importation, including card history, assigned members,
|
||||||
|
labels, comments, and attachments;
|
||||||
* Autocompletion in the minicard editor. Start with <kbd>@</kbd> to start the
|
* Autocompletion in the minicard editor. Start with <kbd>@</kbd> to start the
|
||||||
a board member autocompletion, or <kbd>#</kbd> for a label;
|
a board member autocompletion, or <kbd>#</kbd> for a label;
|
||||||
* Accelerate the initial page rendering by sending the data on the intial HTTP
|
* Accelerate the initial page rendering by sending the data on the intial HTTP
|
||||||
|
|
|
@ -86,7 +86,8 @@ BlazeComponent.extendComponent({
|
||||||
|
|
||||||
attachmentLink() {
|
attachmentLink() {
|
||||||
const attachment = this.currentData().attachment();
|
const attachment = this.currentData().attachment();
|
||||||
return attachment && Blaze.toHTML(HTML.A({
|
// trying to display url before file is stored generates js errors
|
||||||
|
return attachment && attachment.url({ download: true }) && Blaze.toHTML(HTML.A({
|
||||||
href: FlowRouter.path(attachment.url({ download: true })),
|
href: FlowRouter.path(attachment.url({ download: true })),
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
}, attachment.name()));
|
}, attachment.name()));
|
||||||
|
|
|
@ -4,4 +4,51 @@ template(name="importPopup")
|
||||||
form
|
form
|
||||||
p: label(for='import-textarea') {{_ getLabel}}
|
p: label(for='import-textarea') {{_ getLabel}}
|
||||||
textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
|
textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
|
||||||
|
| {{jsonText}}
|
||||||
|
if membersMapping
|
||||||
|
div
|
||||||
|
a.show-mapping
|
||||||
|
| {{_ 'import-show-user-mapping'}}
|
||||||
input.primary.wide(type="submit" value="{{_ 'import'}}")
|
input.primary.wide(type="submit" value="{{_ 'import'}}")
|
||||||
|
|
||||||
|
template(name="mapMembersPopup")
|
||||||
|
.map-members
|
||||||
|
p {{_ 'import-members-map'}}
|
||||||
|
.mapping-list
|
||||||
|
each members
|
||||||
|
.mapping
|
||||||
|
a.source
|
||||||
|
div.full-name
|
||||||
|
= fullName
|
||||||
|
div.username
|
||||||
|
| ({{username}})
|
||||||
|
.wekan
|
||||||
|
if wekan
|
||||||
|
+userAvatar(userId=wekan._id)
|
||||||
|
else
|
||||||
|
a.member.add-member.js-add-members
|
||||||
|
i.fa.fa-plus
|
||||||
|
form
|
||||||
|
input.primary.wide(type="submit" value="{{_ 'done'}}")
|
||||||
|
|
||||||
|
template(name="addMemberPopup")
|
||||||
|
|
||||||
|
template(name="mapMembersAddPopup")
|
||||||
|
.select-member
|
||||||
|
p
|
||||||
|
| {{_ 'import-user-select'}}
|
||||||
|
.js-map-member
|
||||||
|
+esInput(index="users")
|
||||||
|
ul.pop-over-list
|
||||||
|
+esEach(index="users")
|
||||||
|
li.item.js-member-item
|
||||||
|
a.name.js-select-import(title="{{profile.name}} ({{username}})" data-id="{{_id}}")
|
||||||
|
+userAvatar(userId=_id esSearch=true)
|
||||||
|
span.full-name
|
||||||
|
= profile.name
|
||||||
|
| (<span class="username">{{username}}</span>)
|
||||||
|
+ifEsIsSearching(index='users')
|
||||||
|
+spinner
|
||||||
|
+ifEsHasNoResults(index="users")
|
||||||
|
.manage-member-section
|
||||||
|
p.quiet {{_ 'no-results'}}
|
||||||
|
|
|
@ -11,48 +11,122 @@ const ImportPopup = BlazeComponent.extendComponent({
|
||||||
return 'importPopup';
|
return 'importPopup';
|
||||||
},
|
},
|
||||||
|
|
||||||
events() {
|
jsonText() {
|
||||||
return [{
|
return Session.get('import.text');
|
||||||
'submit': (evt) => {
|
},
|
||||||
evt.preventDefault();
|
|
||||||
const dataJson = $(evt.currentTarget).find('.js-import-json').val();
|
membersMapping() {
|
||||||
let dataObject;
|
return Session.get('import.membersToMap');
|
||||||
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);
|
|
||||||
this.onFinish(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onCreated() {
|
onCreated() {
|
||||||
this.error = new ReactiveVar('');
|
this.error = new ReactiveVar('');
|
||||||
|
this.dataToImport = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
onFinish() {
|
||||||
|
Popup.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
onShowMapping(evt) {
|
||||||
|
this._storeText(evt);
|
||||||
|
Popup.open('mapMembers')(evt);
|
||||||
|
},
|
||||||
|
|
||||||
|
onSubmit(evt){
|
||||||
|
evt.preventDefault();
|
||||||
|
const dataJson = this._storeText(evt);
|
||||||
|
let dataObject;
|
||||||
|
try {
|
||||||
|
dataObject = JSON.parse(dataJson);
|
||||||
|
this.setError('');
|
||||||
|
} catch (e) {
|
||||||
|
this.setError('error-json-malformed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(this._hasAllNeededData(dataObject)) {
|
||||||
|
this._import(dataObject);
|
||||||
|
} else {
|
||||||
|
this._prepareAdditionalData(dataObject);
|
||||||
|
Popup.open(this._screenAdditionalData())(evt);
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
events() {
|
||||||
|
return [{
|
||||||
|
submit: this.onSubmit,
|
||||||
|
'click .show-mapping': this.onShowMapping,
|
||||||
|
}];
|
||||||
},
|
},
|
||||||
|
|
||||||
setError(error) {
|
setError(error) {
|
||||||
this.error.set(error);
|
this.error.set(error);
|
||||||
},
|
},
|
||||||
|
|
||||||
onFinish() {
|
_import(dataObject) {
|
||||||
Popup.close();
|
const additionalData = this.getAdditionalData();
|
||||||
|
const membersMapping = this.membersMapping();
|
||||||
|
if (membersMapping) {
|
||||||
|
const mappingById = {};
|
||||||
|
membersMapping.forEach((member) => {
|
||||||
|
if (member.wekan) {
|
||||||
|
mappingById[member.id] = member.wekan._id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
additionalData.membersMapping = mappingById;
|
||||||
|
}
|
||||||
|
Session.set('import.membersToMap', null);
|
||||||
|
Session.set('import.text', null);
|
||||||
|
Meteor.call(this.getMethodName(), dataObject, additionalData,
|
||||||
|
(error, response) => {
|
||||||
|
if (error) {
|
||||||
|
this.setError(error.error);
|
||||||
|
} else {
|
||||||
|
// ensure will display what we just imported
|
||||||
|
Filter.addException(response);
|
||||||
|
this.onFinish(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_hasAllNeededData(dataObject) {
|
||||||
|
// import has no members or they are already mapped
|
||||||
|
return dataObject.members.length === 0 || this.membersMapping();
|
||||||
|
},
|
||||||
|
|
||||||
|
_prepareAdditionalData(dataObject) {
|
||||||
|
// we will work on the list itself (an ordered array of objects)
|
||||||
|
// when a mapping is done, we add a 'wekan' field to the object representing the imported member
|
||||||
|
const membersToMap = dataObject.members;
|
||||||
|
// auto-map based on username
|
||||||
|
membersToMap.forEach((importedMember) => {
|
||||||
|
const wekanUser = Users.findOne({username: importedMember.username});
|
||||||
|
if(wekanUser) {
|
||||||
|
importedMember.wekan = wekanUser;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// store members data and mapping in Session
|
||||||
|
// (we go deep and 2-way, so storing in data context is not a viable option)
|
||||||
|
Session.set('import.membersToMap', membersToMap);
|
||||||
|
return membersToMap;
|
||||||
|
},
|
||||||
|
|
||||||
|
_screenAdditionalData() {
|
||||||
|
return 'mapMembers';
|
||||||
|
},
|
||||||
|
|
||||||
|
_storeText() {
|
||||||
|
const dataJson = this.$('.js-import-json').val();
|
||||||
|
Session.set('import.text', dataJson);
|
||||||
|
return dataJson;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
ImportPopup.extendComponent({
|
ImportPopup.extendComponent({
|
||||||
getAdditionalData() {
|
getAdditionalData() {
|
||||||
const listId = this.data()._id;
|
const listId = this.currentData()._id;
|
||||||
const selector = `#js-list-${this.currentData()._id} .js-minicard:first`;
|
const selector = `#js-list-${this.currentData()._id} .js-minicard:first`;
|
||||||
const firstCardDom = $(selector).get(0);
|
const firstCardDom = $(selector).get(0);
|
||||||
const sortIndex = Utils.calculateIndex(null, firstCardDom).base;
|
const sortIndex = Utils.calculateIndex(null, firstCardDom).base;
|
||||||
|
@ -88,3 +162,110 @@ ImportPopup.extendComponent({
|
||||||
},
|
},
|
||||||
}).register('boardImportBoardPopup');
|
}).register('boardImportBoardPopup');
|
||||||
|
|
||||||
|
const ImportMapMembers = BlazeComponent.extendComponent({
|
||||||
|
members() {
|
||||||
|
return Session.get('import.membersToMap');
|
||||||
|
},
|
||||||
|
_refreshMembers(listOfMembers) {
|
||||||
|
Session.set('import.membersToMap', listOfMembers);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Will look into the list of members to import for the specified memberId,
|
||||||
|
* then set its property to the supplied value.
|
||||||
|
* If unset is true, it will remove the property from the rest of the list as well.
|
||||||
|
*
|
||||||
|
* use:
|
||||||
|
* - memberId = null to use selected member
|
||||||
|
* - value = null to unset a property
|
||||||
|
* - unset = true to ensure property is only set on 1 member at a time
|
||||||
|
*/
|
||||||
|
_setPropertyForMember(property, value, memberId, unset = false) {
|
||||||
|
const listOfMembers = this.members();
|
||||||
|
let finder = null;
|
||||||
|
if(memberId) {
|
||||||
|
finder = (member) => member.id === memberId;
|
||||||
|
} else {
|
||||||
|
finder = (member) => member.selected;
|
||||||
|
}
|
||||||
|
listOfMembers.forEach((member) => {
|
||||||
|
if(finder(member)) {
|
||||||
|
if(value !== null) {
|
||||||
|
member[property] = value;
|
||||||
|
} else {
|
||||||
|
delete member[property];
|
||||||
|
}
|
||||||
|
if(!unset) {
|
||||||
|
// we shortcut if we don't care about unsetting the others
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if(unset) {
|
||||||
|
delete member[property];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
// Session.get gives us a copy, we have to set it back so it sticks
|
||||||
|
this._refreshMembers(listOfMembers);
|
||||||
|
},
|
||||||
|
setSelectedMember(memberId) {
|
||||||
|
return this._setPropertyForMember('selected', true, memberId, true);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* returns the member with specified id,
|
||||||
|
* or the selected member if memberId is not specified
|
||||||
|
*/
|
||||||
|
getMember(memberId = null) {
|
||||||
|
const allMembers = Session.get('import.membersToMap');
|
||||||
|
let finder = null;
|
||||||
|
if(memberId) {
|
||||||
|
finder = (user) => user.id === memberId;
|
||||||
|
} else {
|
||||||
|
finder = (user) => user.selected;
|
||||||
|
}
|
||||||
|
return allMembers.find(finder);
|
||||||
|
},
|
||||||
|
mapSelectedMember(wekan) {
|
||||||
|
return this._setPropertyForMember('wekan', wekan, null);
|
||||||
|
},
|
||||||
|
unmapMember(memberId){
|
||||||
|
return this._setPropertyForMember('wekan', null, memberId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ImportMapMembers.extendComponent({
|
||||||
|
onMapMember(evt) {
|
||||||
|
const memberToMap = this.currentData();
|
||||||
|
if(memberToMap.wekan) {
|
||||||
|
// todo xxx ask for confirmation?
|
||||||
|
this.unmapMember(memberToMap.id);
|
||||||
|
} else {
|
||||||
|
this.setSelectedMember(memberToMap.id);
|
||||||
|
Popup.open('mapMembersAdd')(evt);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmit(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
Popup.back();
|
||||||
|
},
|
||||||
|
events() {
|
||||||
|
return [{
|
||||||
|
'submit': this.onSubmit,
|
||||||
|
'click .mapping': this.onMapMember,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
}).register('mapMembersPopup');
|
||||||
|
|
||||||
|
ImportMapMembers.extendComponent({
|
||||||
|
onSelectUser(){
|
||||||
|
this.mapSelectedMember(this.currentData());
|
||||||
|
Popup.back();
|
||||||
|
},
|
||||||
|
events() {
|
||||||
|
return [{
|
||||||
|
'click .js-select-import': this.onSelectUser,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
onRendered() {
|
||||||
|
// todo XXX why do I not get the focus??
|
||||||
|
this.find('.js-map-member input').focus();
|
||||||
|
},
|
||||||
|
}).register('mapMembersAddPopup');
|
||||||
|
|
17
client/components/import/import.styl
vendored
Normal file
17
client/components/import/import.styl
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.map-members
|
||||||
|
.mapping:first-of-type
|
||||||
|
border-top: solid 1px #999
|
||||||
|
.mapping
|
||||||
|
padding: 10px 0
|
||||||
|
border-bottom: solid 1px #999
|
||||||
|
.source
|
||||||
|
display: inline-block
|
||||||
|
width: 80%
|
||||||
|
.wekan
|
||||||
|
display: inline-block
|
||||||
|
width: 35px
|
||||||
|
.member
|
||||||
|
float: none
|
||||||
|
|
||||||
|
a.show-mapping
|
||||||
|
text-decoration underline
|
|
@ -115,6 +115,7 @@
|
||||||
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
|
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
|
||||||
"disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
|
"disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
|
||||||
"discard": "Discard",
|
"discard": "Discard",
|
||||||
|
"done": "Done",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"edit-avatar": "Change Avatar",
|
"edit-avatar": "Change Avatar",
|
||||||
|
@ -142,6 +143,9 @@
|
||||||
"import-card": "Import a Trello card",
|
"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-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",
|
"import-json-placeholder": "Paste your valid JSON data here",
|
||||||
|
"import-members-map": "Your imported board has some members. Please map the members you want to import to Wekan users",
|
||||||
|
"import-show-user-mapping": "Review members mapping",
|
||||||
|
"import-user-select": "Pick the Wekan user you want to use as this member",
|
||||||
"info": "Infos",
|
"info": "Infos",
|
||||||
"initials": "Initials",
|
"initials": "Initials",
|
||||||
"joined": "joined",
|
"joined": "joined",
|
||||||
|
@ -165,6 +169,8 @@
|
||||||
"lists": "Lists",
|
"lists": "Lists",
|
||||||
"log-out": "Log Out",
|
"log-out": "Log Out",
|
||||||
"loginPopup-title": "Log In",
|
"loginPopup-title": "Log In",
|
||||||
|
"mapMembersPopup-title": "Map members",
|
||||||
|
"mapMembersAddPopup-title": "Select Wekan member",
|
||||||
"memberMenuPopup-title": "Member Settings",
|
"memberMenuPopup-title": "Member Settings",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"menu": "Menu",
|
"menu": "Menu",
|
||||||
|
|
|
@ -108,7 +108,10 @@ Cards.helpers({
|
||||||
},
|
},
|
||||||
|
|
||||||
cover() {
|
cover() {
|
||||||
return Attachments.findOne(this.coverId);
|
const cover = Attachments.findOne(this.coverId);
|
||||||
|
// if we return a cover before it is fully stored, we will get errors when we try to display it
|
||||||
|
// todo XXX we could return a default "upload pending" image in the meantime?
|
||||||
|
return cover && cover.url() && cover;
|
||||||
},
|
},
|
||||||
|
|
||||||
absoluteUrl() {
|
absoluteUrl() {
|
||||||
|
|
263
models/import.js
263
models/import.js
|
@ -4,7 +4,7 @@ const DateString = Match.Where(function (dateAsString) {
|
||||||
});
|
});
|
||||||
|
|
||||||
class TrelloCreator {
|
class TrelloCreator {
|
||||||
constructor() {
|
constructor(data) {
|
||||||
// The object creation dates, indexed by Trello id (so we only parse actions
|
// The object creation dates, indexed by Trello id (so we only parse actions
|
||||||
// once!)
|
// once!)
|
||||||
this.createdAt = {
|
this.createdAt = {
|
||||||
|
@ -18,6 +18,11 @@ class TrelloCreator {
|
||||||
this.lists = {};
|
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 = {};
|
this.comments = {};
|
||||||
|
// the members, indexed by Trello member id => Wekan user ID
|
||||||
|
this.members = data.membersMapping ? data.membersMapping : {};
|
||||||
|
|
||||||
|
// maps a trelloCardId to an array of trelloAttachments
|
||||||
|
this.attachments = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
checkActions(trelloActions) {
|
checkActions(trelloActions) {
|
||||||
|
@ -90,6 +95,24 @@ class TrelloCreator {
|
||||||
stars: 0,
|
stars: 0,
|
||||||
title: trelloBoard.name,
|
title: trelloBoard.name,
|
||||||
};
|
};
|
||||||
|
// now add other members
|
||||||
|
if(trelloBoard.memberships) {
|
||||||
|
trelloBoard.memberships.forEach((trelloMembership) => {
|
||||||
|
const trelloId = trelloMembership.idMember;
|
||||||
|
// do we have a mapping?
|
||||||
|
if(this.members[trelloId]) {
|
||||||
|
const wekanId = this.members[trelloId];
|
||||||
|
// do we already have it in our list?
|
||||||
|
if(!boardToCreate.members.find((wekanMember) => wekanMember.userId === wekanId)) {
|
||||||
|
boardToCreate.members.push({
|
||||||
|
userId: wekanId,
|
||||||
|
isAdmin: false,
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
trelloBoard.labels.forEach((label) => {
|
trelloBoard.labels.forEach((label) => {
|
||||||
const labelToCreate = {
|
const labelToCreate = {
|
||||||
_id: Random.id(6),
|
_id: Random.id(6),
|
||||||
|
@ -121,6 +144,130 @@ class TrelloCreator {
|
||||||
return boardId;
|
return boardId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the Wekan cards corresponding to the supplied Trello cards,
|
||||||
|
* as well as all linked data: activities, comments, and attachments
|
||||||
|
* @param trelloCards
|
||||||
|
* @param boardId
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
createCards(trelloCards, boardId) {
|
||||||
|
const result = [];
|
||||||
|
trelloCards.forEach((card) => {
|
||||||
|
const cardToCreate = {
|
||||||
|
archived: card.closed,
|
||||||
|
boardId,
|
||||||
|
createdAt: new Date(this.createdAt.cards[card.id] || Date.now()),
|
||||||
|
dateLastActivity: new Date(),
|
||||||
|
description: card.desc,
|
||||||
|
listId: this.lists[card.idList],
|
||||||
|
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];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// add members {
|
||||||
|
if(card.idMembers) {
|
||||||
|
const wekanMembers = [];
|
||||||
|
// we can't just map, as some members may not have been mapped
|
||||||
|
card.idMembers.forEach((trelloId) => {
|
||||||
|
if(this.members[trelloId]) {
|
||||||
|
const wekanId = this.members[trelloId];
|
||||||
|
// we may map multiple Trello members to the same wekan user
|
||||||
|
// in which case we risk adding the same user multiple times
|
||||||
|
if(!wekanMembers.find((wId) => wId === wekanId)){
|
||||||
|
wekanMembers.push(wekanId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if(wekanMembers.length>0) {
|
||||||
|
cardToCreate.members = wekanMembers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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(),
|
||||||
|
};
|
||||||
|
// dateLastActivity will be set from activity insert, no need to
|
||||||
|
// update it ourselves
|
||||||
|
const commentId = CardComments.direct.insert(commentToCreate);
|
||||||
|
Activities.direct.insert({
|
||||||
|
activityType: 'addComment',
|
||||||
|
boardId: commentToCreate.boardId,
|
||||||
|
cardId: commentToCreate.cardId,
|
||||||
|
commentId,
|
||||||
|
createdAt: commentToCreate.createdAt,
|
||||||
|
userId: commentToCreate.userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const attachments = this.attachments[card.id];
|
||||||
|
const trelloCoverId = card.idAttachmentCover;
|
||||||
|
if (attachments) {
|
||||||
|
attachments.forEach((att) => {
|
||||||
|
const file = new FS.File();
|
||||||
|
// Simulating file.attachData on the client generates multiple errors
|
||||||
|
// - HEAD returns null, which causes exception down the line
|
||||||
|
// - the template then tries to display the url to the attachment which causes other errors
|
||||||
|
// so we make it server only, and let UI catch up once it is done, forget about latency comp.
|
||||||
|
if(Meteor.isServer) {
|
||||||
|
file.attachData(att.url, function (error) {
|
||||||
|
file.boardId = boardId;
|
||||||
|
file.cardId = cardId;
|
||||||
|
if (error) {
|
||||||
|
throw(error);
|
||||||
|
} else {
|
||||||
|
const wekanAtt = Attachments.insert(file, () => {
|
||||||
|
// we do nothing
|
||||||
|
});
|
||||||
|
//
|
||||||
|
if(trelloCoverId === att.id) {
|
||||||
|
Cards.direct.update(cardId, { $set: {coverId: wekanAtt._id}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// todo XXX set cover - if need be
|
||||||
|
});
|
||||||
|
}
|
||||||
|
result.push(cardId);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
createLabels(trelloLabels, board) {
|
||||||
trelloLabels.forEach((label) => {
|
trelloLabels.forEach((label) => {
|
||||||
|
@ -170,75 +317,6 @@ class TrelloCreator {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createCardsAndComments(trelloCards, boardId) {
|
|
||||||
const result = [];
|
|
||||||
trelloCards.forEach((card) => {
|
|
||||||
const cardToCreate = {
|
|
||||||
archived: card.closed,
|
|
||||||
boardId,
|
|
||||||
createdAt: new Date(this.createdAt.cards[card.id] || Date.now()),
|
|
||||||
dateLastActivity: new Date(),
|
|
||||||
description: card.desc,
|
|
||||||
listId: this.lists[card.idList],
|
|
||||||
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];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 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(),
|
|
||||||
};
|
|
||||||
// dateLastActivity will be set from activity insert, no need to
|
|
||||||
// update it ourselves
|
|
||||||
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
|
|
||||||
result.push(cardId);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
getColor(trelloColorCode) {
|
getColor(trelloColorCode) {
|
||||||
// trello color name => wekan color
|
// trello color name => wekan color
|
||||||
|
@ -269,6 +347,29 @@ class TrelloCreator {
|
||||||
parseActions(trelloActions) {
|
parseActions(trelloActions) {
|
||||||
trelloActions.forEach((action) => {
|
trelloActions.forEach((action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case 'addAttachmentToCard':
|
||||||
|
// We have to be cautious, because the attachment could have been removed later.
|
||||||
|
// In that case Trello still reports its addition, but removes its 'url' field.
|
||||||
|
// So we test for that
|
||||||
|
const trelloAttachment = action.data.attachment;
|
||||||
|
if(trelloAttachment.url) {
|
||||||
|
// we cannot actually create the Wekan attachment, because we don't yet
|
||||||
|
// have the cards to attach it to, so we store it in the instance variable.
|
||||||
|
const trelloCardId = action.data.card.id;
|
||||||
|
if(!this.attachments[trelloCardId]) {
|
||||||
|
this.attachments[trelloCardId] = [];
|
||||||
|
}
|
||||||
|
this.attachments[trelloCardId].push(trelloAttachment);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'commentCard':
|
||||||
|
const id = action.data.card.id;
|
||||||
|
if (this.comments[id]) {
|
||||||
|
this.comments[id].push(action);
|
||||||
|
} else {
|
||||||
|
this.comments[id] = [action];
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'createBoard':
|
case 'createBoard':
|
||||||
this.createdAt.board = action.date;
|
this.createdAt.board = action.date;
|
||||||
break;
|
break;
|
||||||
|
@ -280,14 +381,6 @@ class TrelloCreator {
|
||||||
const listId = action.data.list.id;
|
const listId = action.data.list.id;
|
||||||
this.createdAt.lists[listId] = action.date;
|
this.createdAt.lists[listId] = action.date;
|
||||||
break;
|
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:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
break;
|
break;
|
||||||
|
@ -298,12 +391,13 @@ class TrelloCreator {
|
||||||
|
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
importTrelloBoard(trelloBoard, data) {
|
importTrelloBoard(trelloBoard, data) {
|
||||||
const trelloCreator = new TrelloCreator();
|
const trelloCreator = new TrelloCreator(data);
|
||||||
|
|
||||||
// 1. check all parameters are ok from a syntax point of view
|
// 1. check all parameters are ok from a syntax point of view
|
||||||
try {
|
try {
|
||||||
// we don't use additional data - this should be an empty object
|
check(data, {
|
||||||
check(data, {});
|
membersMapping: Match.Optional(Object),
|
||||||
|
});
|
||||||
trelloCreator.checkActions(trelloBoard.actions);
|
trelloCreator.checkActions(trelloBoard.actions);
|
||||||
trelloCreator.checkBoard(trelloBoard);
|
trelloCreator.checkBoard(trelloBoard);
|
||||||
trelloCreator.checkLabels(trelloBoard.labels);
|
trelloCreator.checkLabels(trelloBoard.labels);
|
||||||
|
@ -320,19 +414,20 @@ Meteor.methods({
|
||||||
trelloCreator.parseActions(trelloBoard.actions);
|
trelloCreator.parseActions(trelloBoard.actions);
|
||||||
const boardId = trelloCreator.createBoardAndLabels(trelloBoard);
|
const boardId = trelloCreator.createBoardAndLabels(trelloBoard);
|
||||||
trelloCreator.createLists(trelloBoard.lists, boardId);
|
trelloCreator.createLists(trelloBoard.lists, boardId);
|
||||||
trelloCreator.createCardsAndComments(trelloBoard.cards, boardId);
|
trelloCreator.createCards(trelloBoard.cards, boardId);
|
||||||
// XXX add members
|
// XXX add members
|
||||||
return boardId;
|
return boardId;
|
||||||
},
|
},
|
||||||
|
|
||||||
importTrelloCard(trelloCard, data) {
|
importTrelloCard(trelloCard, data) {
|
||||||
const trelloCreator = new TrelloCreator();
|
const trelloCreator = new TrelloCreator(data);
|
||||||
|
|
||||||
// 1. check parameters are ok from a syntax point of view
|
// 1. check parameters are ok from a syntax point of view
|
||||||
try {
|
try {
|
||||||
check(data, {
|
check(data, {
|
||||||
listId: String,
|
listId: String,
|
||||||
sortIndex: Number,
|
sortIndex: Number,
|
||||||
|
membersMapping: Match.Optional(Object),
|
||||||
});
|
});
|
||||||
trelloCreator.checkCards([trelloCard]);
|
trelloCreator.checkCards([trelloCard]);
|
||||||
trelloCreator.checkLabels(trelloCard.labels);
|
trelloCreator.checkLabels(trelloCard.labels);
|
||||||
|
@ -358,7 +453,7 @@ Meteor.methods({
|
||||||
trelloCreator.parseActions(trelloCard.actions);
|
trelloCreator.parseActions(trelloCard.actions);
|
||||||
const board = list.board();
|
const board = list.board();
|
||||||
trelloCreator.createLabels(trelloCard.labels, board);
|
trelloCreator.createLabels(trelloCard.labels, board);
|
||||||
const cardIds = trelloCreator.createCardsAndComments([trelloCard], board._id);
|
const cardIds = trelloCreator.createCards([trelloCard], board._id);
|
||||||
return cardIds[0];
|
return cardIds[0];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ Users = Meteor.users; // eslint-disable-line meteor/collections
|
||||||
|
|
||||||
// Search a user in the complete server database by its name or username. This
|
// Search a user in the complete server database by its name or username. This
|
||||||
// is used for instance to add a new user to a board.
|
// is used for instance to add a new user to a board.
|
||||||
const searchInFields = ['username', 'profile.name'];
|
const searchInFields = ['username', 'profile.fullname'];
|
||||||
Users.initEasySearch(searchInFields, {
|
Users.initEasySearch(searchInFields, {
|
||||||
use: 'mongo-db',
|
use: 'mongo-db',
|
||||||
returnFields: [...searchInFields, 'profile.avatarUrl'],
|
returnFields: [...searchInFields, 'profile.avatarUrl'],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue