diff options
author | Maxime Quandalle <maxime@quandalle.com> | 2015-12-08 16:18:44 -0500 |
---|---|---|
committer | Maxime Quandalle <maxime@quandalle.com> | 2016-01-31 20:03:12 +0100 |
commit | a13fad749e8a75025bb13de87f0170e1ea9e462d (patch) | |
tree | 81ef2f15cb540dfdd7efb81571e213d995f7b666 | |
parent | 67e7b6a139280cab1e1bccb94c684c56eb69985c (diff) | |
download | wekan-a13fad749e8a75025bb13de87f0170e1ea9e462d.tar.gz wekan-a13fad749e8a75025bb13de87f0170e1ea9e462d.tar.bz2 wekan-a13fad749e8a75025bb13de87f0170e1ea9e462d.zip |
Change the board import layout from a popup to a full page
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.
-rw-r--r-- | .eslintrc | 1 | ||||
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | client/components/boards/boardHeader.jade | 2 | ||||
-rw-r--r-- | client/components/boards/boardHeader.styl | 2 | ||||
-rw-r--r-- | client/components/import/import.jade | 51 | ||||
-rw-r--r-- | client/components/import/import.js | 205 | ||||
-rw-r--r-- | client/components/import/import.styl | 42 | ||||
-rw-r--r-- | client/components/main/layouts.styl | 8 | ||||
-rw-r--r-- | client/lib/popup.js | 5 | ||||
-rw-r--r-- | config/router.js | 20 | ||||
-rw-r--r-- | i18n/en.i18n.json | 11 | ||||
-rw-r--r-- | models/import.js | 38 | ||||
-rw-r--r-- | server/publications/users.js | 11 |
13 files changed, 199 insertions, 201 deletions
@@ -106,7 +106,6 @@ globals: CSSEvents: true EscapeActions: true Filter: true - Filter: true Mixins: true Modal: true MultiSelection: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ae1def..0714a5f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ This patch release fixes two bugs on Sandstorm: This release features: -* Trello boards and cards importation, including card history, assigned members, - labels, comments, and attachments; +* Trello boards importation, including card history, assigned members, labels, + comments, and attachments; * Invite new users to a board using a email address; * Autocompletion in the minicard editor. Start with <kbd>@</kbd> to start a board member autocompletion, or <kbd>#</kbd> for a label; 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]; diff --git a/config/router.js b/config/router.js index 99d5bff6..7194621b 100644 --- a/config/router.js +++ b/config/router.js @@ -79,6 +79,26 @@ FlowRouter.route('/shortcuts', { }, }); +FlowRouter.route('/import', { + name: 'import', + triggersEnter: [ + AccountsTemplates.ensureSignedIn, + () => { + Session.set('currentBoard', null); + Session.set('currentCard', null); + + Filter.reset(); + EscapeActions.executeAll(); + }, + ], + action() { + BlazeLayout.render('defaultLayout', { + headerBar: 'importHeaderBar', + content: 'import', + }); + }, +}); + FlowRouter.notFound = { action() { BlazeLayout.render('defaultLayout', { content: 'notFound' }); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 109e3b57..0cfeeed2 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -78,7 +78,6 @@ "boardChangeTitlePopup-title": "Rename Board", "boardChangeVisibilityPopup-title": "Change Visibility", "boardChangeWatchPopup-title": "Change Watch", - "boardImportBoardPopup-title": "Import board from Trello", "boardMenuPopup-title": "Board Menu", "boards": "Boards", "bucket-example": "Like “Bucket List” for example", @@ -181,13 +180,14 @@ "home": "Home", "import": "Import", "import-board": "import from Trello", - "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text", - "import-card": "Import a Trello card", - "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text", + "import-board-title": "Import board from Trello", + "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text.", "import-json-placeholder": "Paste your valid JSON data here", + "import-map-members": "Map 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", + "importMapMembersAddPopup-title": "Select Wekan member", "info": "Infos", "initials": "Initials", "joined": "joined", @@ -210,8 +210,6 @@ "lists": "Lists", "log-out": "Log Out", "loginPopup-title": "Log In", - "mapMembersAddPopup-title": "Select Wekan member", - "mapMembersPopup-title": "Map members", "memberMenuPopup-title": "Member Settings", "members": "Members", "menu": "Menu", @@ -224,7 +222,6 @@ "muted-info": "You will never be notified of any changes in this board", "my-boards": "My Boards", "name": "Name", - "name": "Name", "no-archived-cards": "No archived cards.", "no-archived-lists": "No archived lists.", "no-results": "No results", diff --git a/models/import.js b/models/import.js index 4be1273c..fecc5c4d 100644 --- a/models/import.js +++ b/models/import.js @@ -470,42 +470,4 @@ Meteor.methods({ // XXX add members return boardId; }, - - importTrelloCard(trelloCard, data) { - 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); - trelloCreator.checkActions(trelloCard.actions); - } catch(e) { - throw new Meteor.Error('error-json-schema'); - } - - // 2. check parameters are ok from a business point of view (exist & - // authorized) - const list = Lists.findOne(data.listId); - if (!list) { - throw new Meteor.Error('error-list-doesNotExist'); - } - if (Meteor.isServer) { - if (!allowIsBoardMember(Meteor.userId(), Boards.findOne(list.boardId))) { - throw new Meteor.Error('error-board-notAMember'); - } - } - - // 3. create all elements - trelloCreator.lists[trelloCard.idList] = data.listId; - trelloCreator.parseActions(trelloCard.actions); - const board = list.board(); - trelloCreator.createLabels(trelloCard.labels, board); - const cardIds = trelloCreator.createCards([trelloCard], board._id); - return cardIds[0]; - }, }); diff --git a/server/publications/users.js b/server/publications/users.js index e69de29b..4321e32b 100644 --- a/server/publications/users.js +++ b/server/publications/users.js @@ -0,0 +1,11 @@ +Meteor.publish('user-miniprofile', function(userId) { + check(userId, String); + + return Users.find(userId, { + fields: { + 'username': 1, + 'profile.fullname': 1, + 'profile.avatarUrl': 1, + }, + }); +}); |