From 027aacb50e2baf715c89d914e34132458cf11691 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Fri, 23 Oct 2015 03:37:34 +0200 Subject: [PATCH 1/8] Import members: added UI --- client/components/import/import.jade | 8 ++++++ client/components/import/import.js | 42 +++++++++++++++++++++------- i18n/en.i18n.json | 2 ++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/client/components/import/import.jade b/client/components/import/import.jade index f63661afd..799834faa 100644 --- a/client/components/import/import.jade +++ b/client/components/import/import.jade @@ -5,3 +5,11 @@ template(name="importPopup") p: label(for='import-textarea') {{_ getLabel}} textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus) input.primary.wide(type="submit" value="{{_ 'import'}}") + +template(name="mapMembersPopup") + p {{_ 'import-members-map'}} + ul + each members + li.item {{ fullName }} > world + form + input.primary.wide(type="submit" value="{{_ 'import'}}") diff --git a/client/components/import/import.js b/client/components/import/import.js index c6957fa97..f46f15bca 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -13,32 +13,44 @@ const ImportPopup = BlazeComponent.extendComponent({ events() { return [{ - 'submit': (evt) => { + submit(evt) { evt.preventDefault(); const dataJson = $(evt.currentTarget).find('.js-import-json').val(); let dataObject; try { dataObject = JSON.parse(dataJson); + this.setError(''); } 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); + if(dataObject.members.length > 0) { + this.data().toImport = dataObject; + members.forEach( + // todo if there is a Wekan user with same name, add it as a field 'wekanUser' + ); + this.data().members = dataObject.members; + // we bind to preserve data context + Popup.open('mapMembers').bind(this)(evt); + } else { + Meteor.call(this.getMethodName(), dataObject, this.getAdditionalData(), + (error, response) => { + if (error) { + this.setError(error.error); + } else { + Filter.addException(response); + this.onFinish(response); + } } - } - ); + ); + } }, }]; }, onCreated() { this.error = new ReactiveVar(''); + this.dataToImport = ''; }, setError(error) { @@ -88,3 +100,13 @@ ImportPopup.extendComponent({ }, }).register('boardImportBoardPopup'); +BlazeComponent.extendComponent({ + events() { + return [{ + 'submit': (evt) => { + evt.preventDefault(); + console.log(this.data()); + }, + }]; + }, +}).register('mapMembersPopup'); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 0823ba085..d2f953922 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -141,6 +141,7 @@ "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", + "import-members-map": "Map the Trello members you want to import to Wekan members", "info": "Infos", "initials": "Initials", "joined": "joined", @@ -164,6 +165,7 @@ "lists": "Lists", "log-out": "Log Out", "loginPopup-title": "Log In", + "mapMembersPopup-title": "Map members", "memberMenuPopup-title": "Member Settings", "members": "Members", "menu": "Menu", From 3dc70b3ea4d49bccb10d4f2b3e317c794da91e37 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Fri, 13 Nov 2015 12:35:41 +0100 Subject: [PATCH 2/8] Import members: UI flow ok --- .meteor/release | 2 +- .meteor/versions | 65 ++++---- client/components/import/import.jade | 45 +++++- client/components/import/import.js | 214 +++++++++++++++++++------ client/components/import/import.styl | 13 ++ client/components/sidebar/sidebar.jade | 2 +- i18n/en.i18n.json | 1 + models/users.js | 4 +- 8 files changed, 258 insertions(+), 88 deletions(-) create mode 100644 client/components/import/import.styl diff --git a/.meteor/release b/.meteor/release index 5684262a8..1090f5819 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.2.0.2 +METEOR@1.2.2-cdn-url diff --git a/.meteor/versions b/.meteor/versions index 840f09f94..449422954 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,19 +1,19 @@ -3stack:presence@1.0.3 -accounts-base@1.2.1 -accounts-password@1.1.3 +3stack:presence@1.0.4 +accounts-base@1.2.2 +accounts-password@1.1.4 aldeed:collection2@2.5.0 aldeed:simple-schema@1.3.3 alethes:pages@1.8.4 arillo:flow-router-helpers@0.4.5 audit-argument-checks@1.0.4 -autoupdate@1.2.3 +autoupdate@1.2.4 babel-compiler@5.8.24_1 babel-runtime@0.1.4 base64@1.0.4 binary-heap@1.0.4 blaze@2.1.3 blaze-tools@1.0.4 -boilerplate-generator@1.0.4 +boilerplate-generator@1.0.5-cdn-url caching-compiler@1.0.0 caching-html-compiler@1.0.2 callback-hook@1.0.4 @@ -34,22 +34,22 @@ cfs:storage-adapter@0.2.3 cfs:tempstore@0.1.5 cfs:upload-http@0.0.20 cfs:worker@0.1.4 -check@1.0.6 -coffeescript@1.0.10 -cosmos:browserify@0.5.1 -dburles:collection-helpers@1.0.3 +check@1.1.0 +coffeescript@1.0.11 +cosmos:browserify@0.8.3 +dburles:collection-helpers@1.0.4 ddp@1.2.2 ddp-client@1.2.1 -ddp-common@1.2.1 +ddp-common@1.2.2 ddp-rate-limiter@1.0.0 -ddp-server@1.2.1 +ddp-server@1.2.2 deps@1.0.9 diff-sequence@1.0.1 -ecmascript@0.1.5 -ecmascript-collections@0.1.6 +ecmascript@0.1.6 +ecmascript-runtime@0.2.6 ejson@1.0.7 -email@1.0.7 -es5-shim@4.1.13 +email@1.0.8 +es5-shim@4.1.14 fastclick@1.0.7 fortawesome:fontawesome@4.4.0 geojson-utils@1.0.4 @@ -58,20 +58,19 @@ html-tools@1.0.5 htmljs@1.0.5 http@1.1.1 id-map@1.0.4 -idmontie:migrations@1.0.0 +idmontie:migrations@1.0.1 jquery@1.11.4 -jsx@0.1.6 kadira:blaze-layout@2.2.0 -kadira:dochead@1.1.0 -kadira:flow-router@2.7.0 -kenton:accounts-sandstorm@0.1.6 +kadira:dochead@1.3.2 +kadira:flow-router@2.9.0 +kenton:accounts-sandstorm@0.1.8 launch-screen@1.0.4 livedata@1.0.15 localstorage@1.0.5 logging@1.0.8 matb33:collection-hooks@0.8.1 matteodem:easy-search@1.6.4 -meteor@1.1.9 +meteor@1.1.10 meteor-base@1.0.1 meteor-platform@1.2.3 meteorhacks:aggregate@1.3.0 @@ -82,14 +81,14 @@ meteorspark:util@0.2.0 minifiers@1.1.7 minimongo@1.0.10 mobile-status-bar@1.0.6 -mongo@1.1.2 +mongo@1.1.3 mongo-id@1.0.1 mongo-livedata@1.0.9 mousetrap:mousetrap@1.4.6_1 mquandalle:autofocus@1.0.0 mquandalle:collection-mutations@0.1.0 -mquandalle:jade@0.4.4 -mquandalle:jade-compiler@0.4.4 +mquandalle:jade@0.4.5 +mquandalle:jade-compiler@0.4.5 mquandalle:jquery-textcomplete@0.8.0_1 mquandalle:jquery-ui-drag-drop-sort@0.1.0 mquandalle:moment@1.0.0 @@ -102,17 +101,17 @@ observe-sequence@1.0.7 ongoworks:speakingurl@1.1.0 ordered-dict@1.0.4 peerlibrary:assert@0.2.5 -peerlibrary:base-component@0.13.0 -peerlibrary:blaze-components@0.14.0 -peerlibrary:computed-field@0.3.0 +peerlibrary:base-component@0.14.0 +peerlibrary:blaze-components@0.15.1 +peerlibrary:computed-field@0.3.1 peerlibrary:reactive-field@0.1.0 perak:markdown@1.0.5 -promise@0.5.0 +promise@0.5.1 raix:eventemitter@0.1.3 raix:handlebar-helpers@0.2.5 -random@1.0.4 +random@1.0.5 rate-limit@1.0.0 -reactive-dict@1.1.2 +reactive-dict@1.1.3 reactive-var@1.0.6 reload@1.1.4 retry@1.0.4 @@ -126,10 +125,10 @@ softwarerero:accounts-t9n@1.1.4 spacebars@1.0.7 spacebars-compiler@1.0.7 srp@1.0.4 -standard-minifiers@1.0.1 +standard-minifiers@1.0.2 tap:i18n@1.7.0 templates:tabs@2.2.0 -templating@1.1.4 +templating@1.1.5 templating-tools@1.0.0 tracker@1.0.9 ui@1.0.8 @@ -139,6 +138,6 @@ useraccounts:core@1.12.4 useraccounts:flow-routing@1.12.4 useraccounts:unstyled@1.12.4 verron:autosize@3.0.8 -webapp@1.2.2 +webapp@1.2.4-cdn-url webapp-hashing@1.0.5 zimme:active-route@2.3.2 diff --git a/client/components/import/import.jade b/client/components/import/import.jade index 799834faa..07e1116b8 100644 --- a/client/components/import/import.jade +++ b/client/components/import/import.jade @@ -7,9 +7,42 @@ template(name="importPopup") input.primary.wide(type="submit" value="{{_ 'import'}}") template(name="mapMembersPopup") - p {{_ 'import-members-map'}} - ul - each members - li.item {{ fullName }} > world - form - input.primary.wide(type="submit" value="{{_ 'import'}}") + .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="{{_ 'import'}}") + + template(name="addMemberPopup") + +template(name="mapMembersAddPopup") + .select-member + p Hello world + .js-map-member + +esInput(index="users") + ul.pop-over-list + +esEach(index="users") + li.item.js-member-item + a.name.js-select-member(title="{{profile.name}} ({{username}})" data-id="{{_id}}") + +userAvatar(userId=_id esSearch=true) + span.full-name + = profile.name + | ({{username}}) + +ifEsIsSearching(index='users') + +spinner + +ifEsHasNoResults(index="users") + .manage-member-section + p.quiet {{_ 'no-results'}} diff --git a/client/components/import/import.js b/client/components/import/import.js index f46f15bca..3682afdb0 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -11,60 +11,76 @@ const ImportPopup = BlazeComponent.extendComponent({ return 'importPopup'; }, - events() { - return [{ - submit(evt) { - evt.preventDefault(); - const dataJson = $(evt.currentTarget).find('.js-import-json').val(); - let dataObject; - try { - dataObject = JSON.parse(dataJson); - this.setError(''); - } catch (e) { - this.setError('error-json-malformed'); - return; - } - if(dataObject.members.length > 0) { - this.data().toImport = dataObject; - members.forEach( - // todo if there is a Wekan user with same name, add it as a field 'wekanUser' - ); - this.data().members = dataObject.members; - // we bind to preserve data context - Popup.open('mapMembers').bind(this)(evt); - } else { - Meteor.call(this.getMethodName(), dataObject, this.getAdditionalData(), - (error, response) => { - if (error) { - this.setError(error.error); - } else { - Filter.addException(response); - this.onFinish(response); - } - } - ); - } - }, - }]; - }, - onCreated() { this.error = new ReactiveVar(''); this.dataToImport = ''; }, + onFinish() { + Popup.close(); + }, + + onSubmit(evt){ + evt.preventDefault(); + const dataJson = $(evt.currentTarget).find('.js-import-json').val(); + let dataObject; + try { + dataObject = JSON.parse(dataJson); + this.setError(''); + } catch (e) { + this.setError('error-json-malformed'); + return; + } + // if there are members listed in the import, we need to map them + if(dataObject.members.length > 0) { + // we will work on the list itself (an ordered array of POJO) + // when a mapping is done, we add a 'wekan' field to the POJO representing the imported member + const membersToMap = dataObject.members; + // todo save initial import object - to save later, on mapping submission + // this.data().toImport = dataObject; + + // auto-map based on username + const wekanMembers = Users; + 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); + Popup.open('mapMembers')(evt); + } else { + Meteor.call(this.getMethodName(), dataObject, this.getAdditionalData(), + (error, response) => { + if (error) { + this.setError(error.error); + } else { + // ensure will display what we just imported + Filter.addException(response); + this.onFinish(response); + } + } + ); + } + }, + + events() { + return [{ + submit: this.onSubmit, + }]; + }, + setError(error) { this.error.set(error); }, - onFinish() { - Popup.close(); - }, }); ImportPopup.extendComponent({ getAdditionalData() { - const listId = this.data()._id; + const listId = this.currentData()._id; const selector = `#js-list-${this.currentData()._id} .js-minicard:first`; const firstCardDom = $(selector).get(0); const sortIndex = Utils.calculateIndex(null, firstCardDom).base; @@ -100,13 +116,119 @@ ImportPopup.extendComponent({ }, }).register('boardImportBoardPopup'); -BlazeComponent.extendComponent({ +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] = true; + } 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({ + onSelectMember(evt) { + const memberToMap = this.currentData(); + this.setSelectedMember(memberToMap.id); + console.log(`selected member#${memberToMap.id}`); + Popup.open('mapMembersAdd')(evt); + }, + onRemove(evt){ + const userId = this.currentData()._id; + console.log(`confirm and then call unmapMember ${userId}`); + }, + onSubmit(evt) { + console.log("Mapping:"); + console.log(this.members()); + }, events() { return [{ - 'submit': (evt) => { - evt.preventDefault(); - console.log(this.data()); - }, + 'submit': this.onSubmit, + 'click .js-add-members': this.onSelectMember, + 'click .js-member': this.onRemove, }]; }, }).register('mapMembersPopup'); + +ImportMapMembers.extendComponent({ + //template() { + // return "mapMembersAddPopup"; + //}, + onSelectUser(){ + const wekanUser = this.currentData(); + console.log(`clicked on ${wekanUser._id}`); + console.log(wekanUser); + //this.mapSelectedMember(this.currentData()); + }, + events() { + return [{ + //'click .js-select-member': this.onSelectUser(), + }]; + }, + onRendered() { + console.log('rendered'); + // todo XXX why do I not focus?? + $('.js-map-member input').focus(); + }, +}).register('mapMembersAddPopup'); diff --git a/client/components/import/import.styl b/client/components/import/import.styl new file mode 100644 index 000000000..4934cbcca --- /dev/null +++ b/client/components/import/import.styl @@ -0,0 +1,13 @@ +.map-members + .mapping + margin-bottom: 10px + margin-top: 10px + border-bottom: solid + .source + display: inline-block + width: 80% + .wekan + display: inline-block + width: 35px + .member + float: none diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 910470563..f98ea4ee7 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -89,7 +89,7 @@ template(name="addMemberPopup") a.name.js-select-member(title="{{profile.name}} ({{username}})") +userAvatar(userId=_id esSearch=true) span.full-name - = profile.name + = profile.fullname | ({{username}}) if isBoardMember .quiet ({{_ 'joined'}}) diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index eab6ab5a3..14cd9f24a 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -166,6 +166,7 @@ "log-out": "Log Out", "loginPopup-title": "Log In", "mapMembersPopup-title": "Map members", + "mapMembersAddPopup-title": "Select Wekan member", "memberMenuPopup-title": "Member Settings", "members": "Members", "menu": "Menu", diff --git a/models/users.js b/models/users.js index b35104ecf..60cab56dd 100644 --- a/models/users.js +++ b/models/users.js @@ -2,12 +2,14 @@ Users = Meteor.users; // 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. -const searchInFields = ['username', 'profile.name']; +const searchInFields = ['username', 'profile.fullname']; Users.initEasySearch(searchInFields, { use: 'mongo-db', returnFields: [...searchInFields, 'profile.avatarUrl'], }); + + Users.helpers({ boards() { return Boards.find({ userId: this._id }); From f6f41270de1b5a2d3ff6aa4ca7d433915dd29bd9 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Fri, 13 Nov 2015 15:52:14 +0100 Subject: [PATCH 3/8] Import members: working on card import --- client/components/import/import.jade | 9 ++- client/components/import/import.js | 95 +++++++++++++++++----------- models/import.js | 22 ++++++- 3 files changed, 84 insertions(+), 42 deletions(-) diff --git a/client/components/import/import.jade b/client/components/import/import.jade index 07e1116b8..0f53e4d2d 100644 --- a/client/components/import/import.jade +++ b/client/components/import/import.jade @@ -4,6 +4,11 @@ template(name="importPopup") form p: label(for='import-textarea') {{_ getLabel}} textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus) + | {{jsonText}} + if membersMapping + div + a.show-mapping + | {{_ 'show-mapping'}} input.primary.wide(type="submit" value="{{_ 'import'}}") template(name="mapMembersPopup") @@ -24,7 +29,7 @@ template(name="mapMembersPopup") a.member.add-member.js-add-members i.fa.fa-plus form - input.primary.wide(type="submit" value="{{_ 'import'}}") + input.primary.wide(type="submit" value="{{_ 'done'}}") template(name="addMemberPopup") @@ -36,7 +41,7 @@ template(name="mapMembersAddPopup") ul.pop-over-list +esEach(index="users") li.item.js-member-item - a.name.js-select-member(title="{{profile.name}} ({{username}})" data-id="{{_id}}") + a.name.js-select-import(title="{{profile.name}} ({{username}})" data-id="{{_id}}") +userAvatar(userId=_id esSearch=true) span.full-name = profile.name diff --git a/client/components/import/import.js b/client/components/import/import.js index 3682afdb0..b42fcc5dc 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -11,6 +11,14 @@ const ImportPopup = BlazeComponent.extendComponent({ return 'importPopup'; }, + jsonText() { + return Session.get('import.text'); + }, + + membersMapping() { + return Session.get('import.membersToMap'); + }, + onCreated() { this.error = new ReactiveVar(''); this.dataToImport = ''; @@ -20,9 +28,21 @@ const ImportPopup = BlazeComponent.extendComponent({ Popup.close(); }, + onShowMapping(evt) { + // todo xxx make it work - currently we don't find the text + // this._storeText(evt); + Popup.open('mapMembers')(evt); + }, + + _storeText(evt) { + const dataJson = $(evt.currentTarget).find('.js-import-json').val(); + Session.set('import.text', dataJson); + return dataJson; + }, + onSubmit(evt){ evt.preventDefault(); - const dataJson = $(evt.currentTarget).find('.js-import-json').val(); + const dataJson = this._storeText(evt); let dataObject; try { dataObject = JSON.parse(dataJson); @@ -31,18 +51,14 @@ const ImportPopup = BlazeComponent.extendComponent({ this.setError('error-json-malformed'); return; } - // if there are members listed in the import, we need to map them - if(dataObject.members.length > 0) { + // if there are members listed in the import and we have no mapping for them... + if(dataObject.members.length > 0 && !this.membersMapping()) { // we will work on the list itself (an ordered array of POJO) // when a mapping is done, we add a 'wekan' field to the POJO representing the imported member const membersToMap = dataObject.members; - // todo save initial import object - to save later, on mapping submission - // this.data().toImport = dataObject; - // auto-map based on username - const wekanMembers = Users; membersToMap.forEach((importedMember) => { - const wekanUser = Users.findOne({username: importedMember.username}) + const wekanUser = Users.findOne({username: importedMember.username}); if(wekanUser) { importedMember.wekan = wekanUser; } @@ -52,7 +68,20 @@ const ImportPopup = BlazeComponent.extendComponent({ Session.set('import.membersToMap', membersToMap); Popup.open('mapMembers')(evt); } else { - Meteor.call(this.getMethodName(), dataObject, this.getAdditionalData(), + 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); @@ -69,6 +98,7 @@ const ImportPopup = BlazeComponent.extendComponent({ events() { return [{ submit: this.onSubmit, + 'click .show-mapping': this.onShowMapping, }]; }, @@ -144,7 +174,7 @@ const ImportMapMembers = BlazeComponent.extendComponent({ listOfMembers.forEach((member) => { if(finder(member)) { if(value !== null) { - member[property] = true; + member[property] = value; } else { delete member[property]; } @@ -152,10 +182,8 @@ const ImportMapMembers = BlazeComponent.extendComponent({ // we shortcut if we don't care about unsetting the others return false; } - } else { - if(unset) { - delete member[property]; - } + } else if(unset) { + delete member[property]; } return true; }); @@ -188,47 +216,40 @@ const ImportMapMembers = BlazeComponent.extendComponent({ }); ImportMapMembers.extendComponent({ - onSelectMember(evt) { + onMapMember(evt) { const memberToMap = this.currentData(); - this.setSelectedMember(memberToMap.id); - console.log(`selected member#${memberToMap.id}`); - Popup.open('mapMembersAdd')(evt); - }, - onRemove(evt){ - const userId = this.currentData()._id; - console.log(`confirm and then call unmapMember ${userId}`); + if(memberToMap.wekan) { + // todo xxx ask for confirmation? + this.unmapMember(memberToMap.id); + } else { + this.setSelectedMember(memberToMap.id); + Popup.open('mapMembersAdd')(evt); + } }, onSubmit(evt) { - console.log("Mapping:"); - console.log(this.members()); + evt.preventDefault(); + Popup.back(); }, events() { return [{ 'submit': this.onSubmit, - 'click .js-add-members': this.onSelectMember, - 'click .js-member': this.onRemove, + 'click .mapping': this.onMapMember, }]; }, }).register('mapMembersPopup'); ImportMapMembers.extendComponent({ - //template() { - // return "mapMembersAddPopup"; - //}, onSelectUser(){ - const wekanUser = this.currentData(); - console.log(`clicked on ${wekanUser._id}`); - console.log(wekanUser); - //this.mapSelectedMember(this.currentData()); + this.mapSelectedMember(this.currentData()); + Popup.back(); }, events() { return [{ - //'click .js-select-member': this.onSelectUser(), + 'click .js-select-import': this.onSelectUser, }]; }, onRendered() { - console.log('rendered'); - // todo XXX why do I not focus?? - $('.js-map-member input').focus(); + // todo XXX why do I not get the focus?? + this.find('.js-map-member input').focus(); }, }).register('mapMembersAddPopup'); diff --git a/models/import.js b/models/import.js index a6e9f3d5b..ab23f0a94 100644 --- a/models/import.js +++ b/models/import.js @@ -4,7 +4,7 @@ const DateString = Match.Where(function (dateAsString) { }); class TrelloCreator { - constructor() { + constructor(data) { // The object creation dates, indexed by Trello id (so we only parse actions // once!) this.createdAt = { @@ -18,6 +18,8 @@ class TrelloCreator { this.lists = {}; // The comments, indexed by Trello card id (to map when importing cards) this.comments = {}; + // the members, indexed by Trello member id => Wekan user ID + this.members = data.membersMapping ? data.membersMapping : {}; } checkActions(trelloActions) { @@ -191,6 +193,19 @@ class TrelloCreator { 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((id) => { + if(this.members[id]) { + wekanMembers.push(this.members[id]); + } + }); + if(wekanMembers.length>0) { + cardToCreate.members = wekanMembers; + } + } // insert card const cardId = Cards.direct.insert(cardToCreate); // log activity @@ -298,7 +313,7 @@ class TrelloCreator { Meteor.methods({ importTrelloBoard(trelloBoard, data) { - const trelloCreator = new TrelloCreator(); + const trelloCreator = new TrelloCreator(data); // 1. check all parameters are ok from a syntax point of view try { @@ -326,13 +341,14 @@ Meteor.methods({ }, importTrelloCard(trelloCard, data) { - const trelloCreator = new TrelloCreator(); + const trelloCreator = new TrelloCreator(data); // 1. check parameters are ok from a syntax point of view try { check(data, { listId: String, sortIndex: Number, + membersMapping: Match.Optional(Object), }); trelloCreator.checkCards([trelloCard]); trelloCreator.checkLabels(trelloCard.labels); From dd0a6e1a8223cbb80ea940985dc9706d468ed72c Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Fri, 13 Nov 2015 19:17:09 +0100 Subject: [PATCH 4/8] Import members: board import --- .meteor/versions | 3 ++- client/components/import/import.jade | 5 ++-- client/components/import/import.js | 5 ++-- client/components/import/import.styl | 10 +++++--- i18n/en.i18n.json | 5 +++- models/import.js | 35 ++++++++++++++++++++++++---- 6 files changed, 48 insertions(+), 15 deletions(-) diff --git a/.meteor/versions b/.meteor/versions index e5de37208..44934cf38 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -12,6 +12,7 @@ babel-runtime@0.1.4 base64@1.0.4 binary-heap@1.0.4 blaze@2.1.3 +blaze-html-templates@1.0.1 blaze-tools@1.0.4 boilerplate-generator@1.0.5-cdn-url caching-compiler@1.0.0 @@ -124,7 +125,7 @@ seriousm:emoji-continued@1.4.0 service-configuration@1.0.5 session@1.1.1 sha@1.0.4 -softwarerero:accounts-t9n@1.1.4 +softwarerero:accounts-t9n@1.1.6 spacebars@1.0.7 spacebars-compiler@1.0.7 srp@1.0.4 diff --git a/client/components/import/import.jade b/client/components/import/import.jade index 0f53e4d2d..74b6ca13e 100644 --- a/client/components/import/import.jade +++ b/client/components/import/import.jade @@ -8,7 +8,7 @@ template(name="importPopup") if membersMapping div a.show-mapping - | {{_ 'show-mapping'}} + | {{_ 'import-show-user-mapping'}} input.primary.wide(type="submit" value="{{_ 'import'}}") template(name="mapMembersPopup") @@ -35,7 +35,8 @@ template(name="mapMembersPopup") template(name="mapMembersAddPopup") .select-member - p Hello world + p + | {{_ 'import-user-select'}} .js-map-member +esInput(index="users") ul.pop-over-list diff --git a/client/components/import/import.js b/client/components/import/import.js index b42fcc5dc..9de649f2f 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -29,13 +29,12 @@ const ImportPopup = BlazeComponent.extendComponent({ }, onShowMapping(evt) { - // todo xxx make it work - currently we don't find the text - // this._storeText(evt); + this._storeText(evt); Popup.open('mapMembers')(evt); }, _storeText(evt) { - const dataJson = $(evt.currentTarget).find('.js-import-json').val(); + const dataJson = this.$('.js-import-json').val(); Session.set('import.text', dataJson); return dataJson; }, diff --git a/client/components/import/import.styl b/client/components/import/import.styl index 4934cbcca..3c6cfdf3a 100644 --- a/client/components/import/import.styl +++ b/client/components/import/import.styl @@ -1,8 +1,9 @@ .map-members + .mapping:first-of-type + border-top: solid 1px #999 .mapping - margin-bottom: 10px - margin-top: 10px - border-bottom: solid + padding: 10px 0 + border-bottom: solid 1px #999 .source display: inline-block width: 80% @@ -11,3 +12,6 @@ width: 35px .member float: none + +a.show-mapping + text-decoration underline diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 14cd9f24a..e6fd4cccc 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -114,6 +114,7 @@ "disambiguateMultiLabelPopup-title": "Disambiguate Label Action", "disambiguateMultiMemberPopup-title": "Disambiguate Member Action", "discard": "Discard", + "done": "Done", "download": "Download", "edit": "Edit", "edit-avatar": "Change Avatar", @@ -141,7 +142,9 @@ "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", - "import-members-map": "Map the Trello members you want to import to Wekan members", + "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", "initials": "Initials", "joined": "joined", diff --git a/models/import.js b/models/import.js index ab23f0a94..f4e93d5da 100644 --- a/models/import.js +++ b/models/import.js @@ -92,6 +92,24 @@ class TrelloCreator { stars: 0, 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) => {return (wekanMember.userId === wekanId);})) { + boardToCreate.members.push({ + userId: wekanId, + isAdmin: false, + isActive: true, + }); + } + } + }); + } trelloBoard.labels.forEach((label) => { const labelToCreate = { _id: Random.id(6), @@ -197,10 +215,16 @@ class TrelloCreator { if(card.idMembers) { const wekanMembers = []; // we can't just map, as some members may not have been mapped - card.idMembers.forEach((id) => { - if(this.members[id]) { - wekanMembers.push(this.members[id]); + 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) => {return (wId === wekanId);})){ + wekanMembers.push(wekanId); + } } + return true; }); if(wekanMembers.length>0) { cardToCreate.members = wekanMembers; @@ -317,8 +341,9 @@ Meteor.methods({ // 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, {}); + check(data, { + membersMapping: Match.Optional(Object), + }); trelloCreator.checkActions(trelloBoard.actions); trelloCreator.checkBoard(trelloBoard); trelloCreator.checkLabels(trelloBoard.labels); From 475bc70621379733dea364147221cc6a13269994 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Sat, 14 Nov 2015 01:54:59 +0100 Subject: [PATCH 5/8] Fix build --- client/components/import/import.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/import/import.js b/client/components/import/import.js index 9de649f2f..038f485c4 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -33,7 +33,7 @@ const ImportPopup = BlazeComponent.extendComponent({ Popup.open('mapMembers')(evt); }, - _storeText(evt) { + _storeText() { const dataJson = this.$('.js-import-json').val(); Session.set('import.text', dataJson); return dataJson; From ad27a59e5790d26f23eb48c2d71d9d3c0a01e81f Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Mon, 16 Nov 2015 21:41:49 +0100 Subject: [PATCH 6/8] Import attachments --- client/components/activities/activities.js | 3 +- models/cards.js | 5 +- models/import.js | 176 ++++++++++++++------- 3 files changed, 121 insertions(+), 63 deletions(-) diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index 64e9865d7..c1465b04b 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -86,7 +86,8 @@ BlazeComponent.extendComponent({ attachmentLink() { 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 })), target: '_blank', }, attachment.name())); diff --git a/models/cards.js b/models/cards.js index 2e16583d5..1895fc696 100644 --- a/models/cards.js +++ b/models/cards.js @@ -108,7 +108,10 @@ Cards.helpers({ }, 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() { diff --git a/models/import.js b/models/import.js index f4e93d5da..c6b94e3fd 100644 --- a/models/import.js +++ b/models/import.js @@ -20,6 +20,9 @@ class TrelloCreator { 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) { @@ -141,56 +144,14 @@ 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 = { - archived: list.closed, - boardId, - // 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(), - }; - const listId = Lists.direct.insert(listToCreate); - const now = new Date(); - Lists.direct.update(listId, {$set: {'updatedAt': now}}); - this.lists[list.id] = listId; - // log activity - Activities.direct.insert({ - activityType: 'importList', - boardId, - createdAt: now, - listId, - source: { - id: list.id, - system: 'Trello', - }, - // We attribute the import to current user, not the one from the - // original object - userId: Meteor.userId(), - }); - }); - } - - createCardsAndComments(trelloCards, 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 = { @@ -273,12 +234,90 @@ class TrelloCreator { }); }); } - // XXX add attachments + 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. + 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 = { + archived: list.closed, + boardId, + // 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(), + }; + const listId = Lists.direct.insert(listToCreate); + const now = new Date(); + Lists.direct.update(listId, {$set: {'updatedAt': now}}); + this.lists[list.id] = listId; + // log activity + Activities.direct.insert({ + activityType: 'importList', + boardId, + createdAt: now, + listId, + source: { + id: list.id, + system: 'Trello', + }, + // We attribute the import to current user, not the one from the + // original object + userId: Meteor.userId(), + }); + }); + } + + getColor(trelloColorCode) { // trello color name => wekan color const mapColors = { @@ -308,6 +347,29 @@ class TrelloCreator { parseActions(trelloActions) { trelloActions.forEach((action) => { 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': this.createdAt.board = action.date; break; @@ -319,14 +381,6 @@ class TrelloCreator { 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; @@ -360,7 +414,7 @@ Meteor.methods({ trelloCreator.parseActions(trelloBoard.actions); const boardId = trelloCreator.createBoardAndLabels(trelloBoard); trelloCreator.createLists(trelloBoard.lists, boardId); - trelloCreator.createCardsAndComments(trelloBoard.cards, boardId); + trelloCreator.createCards(trelloBoard.cards, boardId); // XXX add members return boardId; }, @@ -399,7 +453,7 @@ Meteor.methods({ trelloCreator.parseActions(trelloCard.actions); const board = list.board(); trelloCreator.createLabels(trelloCard.labels, board); - const cardIds = trelloCreator.createCardsAndComments([trelloCard], board._id); + const cardIds = trelloCreator.createCards([trelloCard], board._id); return cardIds[0]; }, }); From 33193b6f7b610dc463f55e46272c865ae8d1fd44 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Tue, 17 Nov 2015 08:52:55 +0100 Subject: [PATCH 7/8] code review fixes --- .meteor/release | 2 +- .meteor/versions | 4 +- client/components/import/import.js | 109 +++++++++++++++++------------ models/import.js | 4 +- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/.meteor/release b/.meteor/release index 1090f5819..3a05e0a2f 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.2.2-cdn-url +METEOR@1.2.1 diff --git a/.meteor/versions b/.meteor/versions index 44934cf38..a16f56bda 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -14,7 +14,7 @@ binary-heap@1.0.4 blaze@2.1.3 blaze-html-templates@1.0.1 blaze-tools@1.0.4 -boilerplate-generator@1.0.5-cdn-url +boilerplate-generator@1.0.4 caching-compiler@1.0.0 caching-html-compiler@1.0.2 callback-hook@1.0.4 @@ -142,6 +142,6 @@ useraccounts:core@1.12.4 useraccounts:flow-routing@1.12.4 useraccounts:unstyled@1.12.4 verron:autosize@3.0.8 -webapp@1.2.4-cdn-url +webapp@1.2.3 webapp-hashing@1.0.5 zimme:active-route@2.3.2 diff --git a/client/components/import/import.js b/client/components/import/import.js index 038f485c4..bb7d06175 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -33,12 +33,6 @@ const ImportPopup = BlazeComponent.extendComponent({ Popup.open('mapMembers')(evt); }, - _storeText() { - const dataJson = this.$('.js-import-json').val(); - Session.set('import.text', dataJson); - return dataJson; - }, - onSubmit(evt){ evt.preventDefault(); const dataJson = this._storeText(evt); @@ -50,47 +44,12 @@ const ImportPopup = BlazeComponent.extendComponent({ this.setError('error-json-malformed'); return; } - // if there are members listed in the import and we have no mapping for them... - if(dataObject.members.length > 0 && !this.membersMapping()) { - // we will work on the list itself (an ordered array of POJO) - // when a mapping is done, we add a 'wekan' field to the POJO 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); - Popup.open('mapMembers')(evt); + if(this._hasAllNeededData(dataObject)) { + this._import(dataObject); } else { - 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); - } - } - ); + this._prepareAdditionalData(dataObject); + Popup.open(this._screenAdditionalData())(evt); + } }, @@ -105,6 +64,64 @@ const ImportPopup = BlazeComponent.extendComponent({ this.error.set(error); }, + _import: function (dataObject) { + 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({ diff --git a/models/import.js b/models/import.js index c6b94e3fd..33f56d4bb 100644 --- a/models/import.js +++ b/models/import.js @@ -103,7 +103,7 @@ class TrelloCreator { if(this.members[trelloId]) { const wekanId = this.members[trelloId]; // do we already have it in our list? - if(!boardToCreate.members.find((wekanMember) => {return (wekanMember.userId === wekanId);})) { + if(!boardToCreate.members.find((wekanMember) => wekanMember.userId === wekanId)) { boardToCreate.members.push({ userId: wekanId, isAdmin: false, @@ -181,7 +181,7 @@ class TrelloCreator { 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) => {return (wId === wekanId);})){ + if(!wekanMembers.find((wId) => wId === wekanId)){ wekanMembers.push(wekanId); } } From db90771d9b1330eb8f7183c472545277b8ed9449 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Tue, 17 Nov 2015 08:56:58 +0100 Subject: [PATCH 8/8] Fix eslint --- client/components/import/import.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/import/import.js b/client/components/import/import.js index bb7d06175..63285e577 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -64,7 +64,7 @@ const ImportPopup = BlazeComponent.extendComponent({ this.error.set(error); }, - _import: function (dataObject) { + _import(dataObject) { const additionalData = this.getAdditionalData(); const membersMapping = this.membersMapping(); if (membersMapping) {