2
0
Эх сурвалжийг харах

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.
Maxime Quandalle 9 жил өмнө
parent
commit
a13fad749e

+ 0 - 1
.eslintrc

@@ -106,7 +106,6 @@ globals:
   CSSEvents: true
   EscapeActions: true
   Filter: true
-  Filter: true
   Mixins: true
   Modal: true
   MultiSelection: true

+ 2 - 2
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;

+ 1 - 1
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")

+ 0 - 2
client/components/boards/boardHeader.styl

@@ -1,2 +0,0 @@
-a.js-import
-  text-decoration underline

+ 32 - 19
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'}}

+ 82 - 123
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');

+ 36 - 6
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
 

+ 6 - 2
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
 

+ 5 - 0
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];

+ 20 - 0
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' });

+ 4 - 7
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",

+ 0 - 38
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];
-  },
 });

+ 11 - 0
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,
+    },
+  });
+});