diff options
Diffstat (limited to 'client/components')
-rw-r--r-- | client/components/activities/activities.js | 3 | ||||
-rw-r--r-- | client/components/import/import.jade | 47 | ||||
-rw-r--r-- | client/components/import/import.js | 235 | ||||
-rw-r--r-- | client/components/import/import.styl | 17 |
4 files changed, 274 insertions, 28 deletions
diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index 64e9865d..c1465b04 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/client/components/import/import.jade b/client/components/import/import.jade index f63661af..74b6ca13 100644 --- a/client/components/import/import.jade +++ b/client/components/import/import.jade @@ -4,4 +4,51 @@ 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 + | {{_ 'import-show-user-mapping'}} 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'}} diff --git a/client/components/import/import.js b/client/components/import/import.js index c6957fa9..63285e57 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -11,48 +11,122 @@ 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); - } 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); - } - } - ); - }, - }]; + jsonText() { + return Session.get('import.text'); + }, + + membersMapping() { + return Session.get('import.membersToMap'); }, onCreated() { 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) { this.error.set(error); }, - onFinish() { - Popup.close(); + _import(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({ 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; @@ -88,3 +162,110 @@ ImportPopup.extendComponent({ }, }).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'); diff --git a/client/components/import/import.styl b/client/components/import/import.styl new file mode 100644 index 00000000..3c6cfdf3 --- /dev/null +++ b/client/components/import/import.styl @@ -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 |