From a13fad749e8a75025bb13de87f0170e1ea9e462d Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 8 Dec 2015 16:18:44 -0500 Subject: Change the board import layout from a popup to a full page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit also removes the “import a single Trello card” as we couldn’t figure out some reasonable use case. We also create a new publication on the server to provide the minimal user profile informations required to display an avatar. --- client/components/boards/boardHeader.jade | 2 +- client/components/boards/boardHeader.styl | 2 - client/components/import/import.jade | 51 +++++--- client/components/import/import.js | 205 ++++++++++++------------------ client/components/import/import.styl | 42 +++++- client/components/main/layouts.styl | 8 +- client/lib/popup.js | 5 + 7 files changed, 162 insertions(+), 153 deletions(-) delete mode 100644 client/components/boards/boardHeader.styl (limited to 'client') diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index 9fc36876..094dad7d 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -175,7 +175,7 @@ template(name="createBoardPopup") input.primary.wide(type="submit" value="{{_ 'create'}}") span.quiet | {{_ 'or'}} - a.js-import {{_ 'import-board'}} + a(href="{{pathFor 'import'}}") {{_ 'import-board'}} template(name="boardChangeTitlePopup") diff --git a/client/components/boards/boardHeader.styl b/client/components/boards/boardHeader.styl deleted file mode 100644 index adfe4b19..00000000 --- a/client/components/boards/boardHeader.styl +++ /dev/null @@ -1,2 +0,0 @@ -a.js-import - text-decoration underline diff --git a/client/components/import/import.jade b/client/components/import/import.jade index 74b6ca13..816a0b45 100644 --- a/client/components/import/import.jade +++ b/client/components/import/import.jade @@ -1,39 +1,52 @@ -template(name="importPopup") - if error.get - .warning {{_ error.get}} +template(name="importHeaderBar") + h1 + a.back-btn(href="{{pathFor 'home'}}") + i.fa.fa-chevron-left + | {{_ 'import-board-title'}} + +template(name="import") + .wrapper + if error.get + .warning {{_ error.get}} + +Template.dynamic(template=currentTemplate) + +template(name="importTextarea") form - p: label(for='import-textarea') {{_ getLabel}} - textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus) + p: label(for='import-textarea') {{_ 'import-board-trello-instruction'}} + 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") +template(name="importMapMembers") + h2 {{_ 'import-map-members'}} .map-members p {{_ 'import-members-map'}} .mapping-list each members - .mapping - a.source - div.full-name - = fullName - div.username + a.mapping-item.js-select-member(class="{{#if wekan}}filled{{/if}}") + .profile-source + .full-name= fullName + .username | ({{username}}) .wekan if wekan +userAvatar(userId=wekan._id) else - a.member.add-member.js-add-members + a.member.add-member i.fa.fa-plus + //- + Due to the way the flewbox layout is working, we need to set some + invisible items so that the last row items have a consistent width. + See http://jsfiddle.net/Ln4h3c4n/ for an minimal example of the issue. + .mapping-item.ghost-item + .mapping-item.ghost-item + .mapping-item.ghost-item + .mapping-item.ghost-item + .mapping-item.ghost-item form input.primary.wide(type="submit" value="{{_ 'done'}}") - template(name="addMemberPopup") - -template(name="mapMembersAddPopup") +template(name="importMapMembersAddPopup") .select-member p | {{_ 'import-user-select'}} diff --git a/client/components/import/import.js b/client/components/import/import.js index ec469a77..169f9dd0 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -1,68 +1,46 @@ -/// 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({ - jsonText() { - return Session.get('import.text'); - }, - - membersMapping() { - return Session.get('import.membersToMap'); - }, - +BlazeComponent.extendComponent({ onCreated() { this.error = new ReactiveVar(''); - this.dataToImport = ''; + this.steps = ['importTextarea', 'importMapMembers']; + this._currentStepIndex = new ReactiveVar(0); + this.importedData = new ReactiveVar(); + this.membersToMap = new ReactiveVar([]); }, - onFinish() { - Popup.close(); + currentTemplate() { + return this.steps[this._currentStepIndex.get()]; }, - onShowMapping(evt) { - this._storeText(evt); - Popup.open('mapMembers')(evt); + nextStep() { + const nextStepIndex = this._currentStepIndex.get() + 1; + if (nextStepIndex >= this.steps.length) { + this.finishImport(); + } else { + this._currentStepIndex.set(nextStepIndex); + } }, - onSubmit(evt){ + importData(evt) { evt.preventDefault(); - const dataJson = this._storeText(evt); - let dataObject; + const dataJson = this.find('.js-import-json').value; try { - dataObject = JSON.parse(dataJson); + const dataObject = JSON.parse(dataJson); this.setError(''); + this.importedData.set(dataObject); + this._prepareAdditionalData(dataObject); + this.nextStep(); } 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); }, - _import(dataObject) { - const additionalData = this.getAdditionalData(); - const membersMapping = this.membersMapping(); + finishImport() { + const additionalData = {}; + const membersMapping = this.membersToMap.get(); if (membersMapping) { const mappingById = {}; membersMapping.forEach((member) => { @@ -72,99 +50,75 @@ const ImportPopup = BlazeComponent.extendComponent({ }); 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); + this.membersToMap.set([]); + Meteor.call('importTrelloBoard', this.importedData.get(), additionalData, + (err, res) => { + if (err) { + this.setError(err.error); } else { - // ensure will display what we just imported - Filter.addException(response); - this.onFinish(response); + Utils.goBoardId(res); } } ); }, - _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 + // 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) { + 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); + this.membersToMap.set(membersToMap); return membersToMap; }, _screenAdditionalData() { return 'mapMembers'; }, +}).register('import'); - _storeText() { - const dataJson = this.$('.js-import-json').val(); - Session.set('import.text', dataJson); - return dataJson; - }, -}); - -ImportPopup.extendComponent({ - getAdditionalData() { - 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; - const result = {listId, sortIndex}; - return result; - }, - - getMethodName() { - return 'importTrelloCard'; +BlazeComponent.extendComponent({ + template() { + return 'importTextarea'; }, - getLabel() { - return 'import-card-trello-instruction'; - }, -}).register('listImportCardPopup'); - -ImportPopup.extendComponent({ - getAdditionalData() { - const result = {}; - return result; - }, - - getMethodName() { - return 'importTrelloBoard'; - }, - - getLabel() { - return 'import-board-trello-instruction'; + events() { + return [{ + submit(evt) { + return this.parentComponent().importData(evt); + }, + }]; }, +}).register('importTextarea'); - onFinish(response) { - Utils.goBoardId(response); +BlazeComponent.extendComponent({ + onCreated() { + this.autorun(() => { + this.parentComponent().membersToMap.get().forEach(({ wekan }) => { + if (wekan !== undefined) { + const userId = wekan._id; + this.subscribe('user-miniprofile', userId); + } + }); + }); }, -}).register('boardImportBoardPopup'); -const ImportMapMembers = BlazeComponent.extendComponent({ members() { - return Session.get('import.membersToMap'); + return this.parentComponent().membersToMap.get(); }, + _refreshMembers(listOfMembers) { - Session.set('import.membersToMap', listOfMembers); + return this.parentComponent().membersToMap.set(listOfMembers); }, + /** * Will look into the list of members to import for the specified memberId, * then set its property to the supplied value. @@ -202,15 +156,17 @@ const ImportMapMembers = BlazeComponent.extendComponent({ // 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'); + const allMembers = this.members(); let finder = null; if(memberId) { finder = (user) => user.id === memberId; @@ -219,15 +175,20 @@ const ImportMapMembers = BlazeComponent.extendComponent({ } return allMembers.find(finder); }, + mapSelectedMember(wekan) { return this._setPropertyForMember('wekan', wekan, null); }, + unmapMember(memberId){ return this._setPropertyForMember('wekan', null, memberId); }, -}); -ImportMapMembers.extendComponent({ + onSubmit(evt) { + evt.preventDefault(); + this.parentComponent().nextStep(); + }, + onMapMember(evt) { const memberToMap = this.currentData(); if(memberToMap.wekan) { @@ -235,33 +196,31 @@ ImportMapMembers.extendComponent({ this.unmapMember(memberToMap.id); } else { this.setSelectedMember(memberToMap.id); - Popup.open('mapMembersAdd')(evt); + Popup.open('importMapMembersAdd')(evt); } }, - onSubmit(evt) { - evt.preventDefault(); - Popup.back(); - }, + events() { return [{ 'submit': this.onSubmit, - 'click .mapping': this.onMapMember, + 'click .js-select-member': this.onMapMember, }]; }, -}).register('mapMembersPopup'); +}).register('importMapMembers'); + +BlazeComponent.extendComponent({ + onRendered() { + this.find('.js-map-member input').focus(); + }, -ImportMapMembers.extendComponent({ onSelectUser(){ - this.mapSelectedMember(this.currentData()); + Popup.getOpenerComponent().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'); +}).register('importMapMembersAddPopup'); diff --git a/client/components/import/import.styl b/client/components/import/import.styl index 187ec6c4..a0de74bb 100644 --- a/client/components/import/import.styl +++ b/client/components/import/import.styl @@ -1,17 +1,47 @@ @import 'nib' .map-members - .mapping:first-of-type - border-top: solid 1px #999 - .mapping - padding: 10px 0 - border-bottom: solid 1px #999 - .source + &:after + content: ""; + flex: auto; + + .mapping-list + display: flex + flex-wrap: wrap + margin: 0 -4px + + .mapping-item + max-width: 300px + min-width: 200px + padding: 6px + margin: 5px + flex:1 + background: white + border-radius: 3px + box-shadow: 0 1px 2px rgba(0,0,0,.15) + + &:hover + background: darken(white, 5%) + + &.filled + background: #E0FFE5 + + &:hover + background: #FFE0E0 + + &.ghost-item + height: 0 + visibility: hidden + border: none + + .profile-source display: inline-block width: 80% + .wekan display: inline-block width: 35px + .member float: none diff --git a/client/components/main/layouts.styl b/client/components/main/layouts.styl index e8d9ab5d..83d4d693 100644 --- a/client/components/main/layouts.styl +++ b/client/components/main/layouts.styl @@ -26,13 +26,14 @@ body #content position: relative flex: 1 - overflow: hidden + overflow-x: hidden .sk-spinner margin-top: 30vh > .wrapper - margin-top: 25px + margin-top: 10px + padding: 15px #modal position: absolute @@ -109,6 +110,9 @@ a cursor: default text-decoration: none +span a + text-decoration: underline + strong font-weight: bold diff --git a/client/lib/popup.js b/client/lib/popup.js index 797eb26d..7cceaa4f 100644 --- a/client/lib/popup.js +++ b/client/lib/popup.js @@ -142,6 +142,11 @@ window.Popup = new class { } } + getOpenerComponent() { + const { openerElement } = Template.parentData(4); + return BlazeComponent.getComponentForElement(openerElement); + } + // An utility fonction that returns the top element of the internal stack _getTopStack() { return this._stack[this._stack.length - 1]; -- cgit v1.2.3-1-g7c22