Explorar o código

Add UI for importing card-as-card and board-as-card

Andrés Manelli %!s(int64=7) %!d(string=hai) anos
pai
achega
dcc7b2970f

+ 3 - 0
client/components/forms/forms.styl

@@ -225,9 +225,12 @@ textarea
 
 
 .edit-controls,
 .edit-controls,
 .add-controls
 .add-controls
+  display: flex
+  align-items: baseline
   margin-top: 0
   margin-top: 0
 
 
   button[type=submit]
   button[type=submit]
+  input[type=button]
     float: left
     float: left
     height: 32px
     height: 32px
     margin-top: -2px
     margin-top: -2px

+ 11 - 0
client/components/lists/list.styl

@@ -187,3 +187,14 @@
       padding: 7px
       padding: 7px
       top: -@padding
       top: -@padding
       right: 17px
       right: 17px
+
+.import-board-wrapper
+  display: flex
+  align-items: baseline
+
+  .js-import-board
+    margin-left: 15px
+
+.search-card-results
+  max-height: 250px
+  overflow: hidden

+ 52 - 1
client/components/lists/listBody.jade

@@ -34,8 +34,59 @@ template(name="addCardForm")
 
 
   .add-controls.clearfix
   .add-controls.clearfix
     button.primary.confirm(type="submit") {{_ 'add'}}
     button.primary.confirm(type="submit") {{_ 'add'}}
-    a.fa.fa-times-thin.js-close-inlined-form
+    span.quiet
+      | {{_ 'or'}}
+      a.js-import {{_ 'import'}}
 
 
 template(name="autocompleteLabelLine")
 template(name="autocompleteLabelLine")
   .minicard-label(class="card-label-{{colorName}}" title=labelName)
   .minicard-label(class="card-label-{{colorName}}" title=labelName)
   span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
   span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
+
+template(name="importCardPopup")
+  label {{_ 'boards'}}:
+  .import-board-wrapper
+    select.js-select-boards
+      each boards
+        if $eq _id currentBoard._id
+          option(value="{{_id}}" selected) {{_ 'current'}}
+        else
+          option(value="{{_id}}") {{title}}
+    input.primary.confirm.js-import-board(type="submit" value="{{_ 'add'}}")
+
+  label {{_ 'swimlanes'}}:
+  select.js-select-swimlanes
+    each swimlanes
+      option(value="{{_id}}") {{title}}
+
+  label {{_ 'lists'}}:
+  select.js-select-lists
+    each lists
+      option(value="{{_id}}") {{title}}
+
+  label {{_ 'cards'}}:
+  select.js-select-lists
+    each cards
+      option(value="{{_id}}") {{title}}
+
+  .edit-controls.clearfix
+    input.primary.confirm.js-done(type="submit" value="{{_ 'done'}}")
+    span.quiet
+      | {{_ 'or'}}
+      a.js-search {{_ 'search'}}
+
+template(name="searchCardPopup")
+  label {{_ 'boards'}}:
+  .import-board-wrapper
+    select.js-select-boards
+      each boards
+        if $eq _id currentBoard._id
+          option(value="{{_id}}" selected) {{_ 'current'}}
+        else
+          option(value="{{_id}}") {{title}}
+  form.js-search-term-form
+    input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus)
+  .list-body.js-perfect-scrollbar.search-card-results
+    .minicards.clearfix.js-minicards
+      each results
+        a.minicard-wrapper.js-minicard
+          +minicard(this)

+ 106 - 0
client/components/lists/listBody.js

@@ -1,3 +1,5 @@
+const subManager = new SubsManager();
+
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   mixins() {
   mixins() {
     return [Mixins.PerfectScrollbar];
     return [Mixins.PerfectScrollbar];
@@ -55,6 +57,7 @@ BlazeComponent.extendComponent({
         boardId: boardId._id,
         boardId: boardId._id,
         sort: sortIndex,
         sort: sortIndex,
         swimlaneId,
         swimlaneId,
+        type: 'cardType-card',
       });
       });
       // In case the filter is active we need to add the newly inserted card in
       // In case the filter is active we need to add the newly inserted card in
       // the list of exceptions -- cards that are not filtered. Otherwise the
       // the list of exceptions -- cards that are not filtered. Otherwise the
@@ -197,6 +200,7 @@ BlazeComponent.extendComponent({
   events() {
   events() {
     return [{
     return [{
       keydown: this.pressKey,
       keydown: this.pressKey,
+      'click .js-import': Popup.open('importCard'),
     }];
     }];
   },
   },
 
 
@@ -268,3 +272,105 @@ BlazeComponent.extendComponent({
     });
     });
   },
   },
 }).register('addCardForm');
 }).register('addCardForm');
+
+BlazeComponent.extendComponent({
+  onCreated() {
+    subManager.subscribe('board', Session.get('currentBoard'));
+    this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
+  },
+
+  boards() {
+    const boards = Boards.find({
+      archived: false,
+      'members.userId': Meteor.userId(),
+    }, {
+      sort: ['title'],
+    });
+    return boards;
+  },
+
+  swimlanes() {
+    const board = Boards.findOne(this.selectedBoardId.get());
+    return board.swimlanes();
+  },
+
+  lists() {
+    const board = Boards.findOne(this.selectedBoardId.get());
+    return board.lists();
+  },
+
+  cards() {
+    const board = Boards.findOne(this.selectedBoardId.get());
+    return board.cards();
+  },
+
+  events() {
+    return [{
+      'change .js-select-boards'(evt) {
+        this.selectedBoardId.set($(evt.currentTarget).val());
+        subManager.subscribe('board', this.selectedBoardId.get());
+      },
+      'submit .js-done' (evt) {
+        // IMPORT CARD
+        evt.preventDefault();
+        // XXX We should *not* get the currentCard from the global state, but
+        // instead from a “component” state.
+        const card = Cards.findOne(Session.get('currentCard'));
+        const lSelect = $('.js-select-lists')[0];
+        const newListId = lSelect.options[lSelect.selectedIndex].value;
+        const slSelect = $('.js-select-swimlanes')[0];
+        card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
+        Popup.close();
+      },
+      'submit .js-import-board' (evt) {
+        //IMPORT BOARD
+        evt.preventDefault();
+        Popup.close();
+      },
+      'click .js-search': Popup.open('searchCard'),
+    }];
+  },
+}).register('importCardPopup');
+
+BlazeComponent.extendComponent({
+  mixins() {
+    return [Mixins.PerfectScrollbar];
+  },
+
+  onCreated() {
+    subManager.subscribe('board', Session.get('currentBoard'));
+    this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
+    this.term = new ReactiveVar('');
+  },
+
+  boards() {
+    const boards = Boards.find({
+      archived: false,
+      'members.userId': Meteor.userId(),
+    }, {
+      sort: ['title'],
+    });
+    return boards;
+  },
+
+  results() {
+    const board = Boards.findOne(this.selectedBoardId.get());
+    return board.searchCards(this.term.get());
+  },
+
+  events() {
+    return [{
+      'change .js-select-boards'(evt) {
+        this.selectedBoardId.set($(evt.currentTarget).val());
+        subManager.subscribe('board', this.selectedBoardId.get());
+      },
+      'submit .js-search-term-form'(evt) {
+        evt.preventDefault();
+        this.term.set(evt.target.searchTerm.value);
+      },
+      'click .js-minicard'() {
+        // IMPORT CARD
+      },
+    }];
+  },
+}).register('searchCardPopup');

+ 5 - 0
i18n/en.i18n.json

@@ -135,6 +135,9 @@
     "cards": "Cards",
     "cards": "Cards",
     "cards-count": "Cards",
     "cards-count": "Cards",
     "casSignIn" : "Sign In with CAS",
     "casSignIn" : "Sign In with CAS",
+    "cardType-card": "Card",
+    "cardType-importedCard": "Imported Card",
+    "cardType-importedBoard": "Imported Board",
     "change": "Change",
     "change": "Change",
     "change-avatar": "Change Avatar",
     "change-avatar": "Change Avatar",
     "change-password": "Change Password",
     "change-password": "Change Password",
@@ -171,6 +174,8 @@
     "confirm-subtask-delete-dialog": "Are you sure you want to delete subtask?",
     "confirm-subtask-delete-dialog": "Are you sure you want to delete subtask?",
     "confirm-checklist-delete-dialog": "Are you sure you want to delete checklist?",
     "confirm-checklist-delete-dialog": "Are you sure you want to delete checklist?",
     "copy-card-link-to-clipboard": "Copy card link to clipboard",
     "copy-card-link-to-clipboard": "Copy card link to clipboard",
+    "importCardPopup-title": "Import Card",
+    "searchCardPopup-title": "Search Card",
     "copyCardPopup-title": "Copy Card",
     "copyCardPopup-title": "Copy Card",
     "copyChecklistToManyCardsPopup-title": "Copy Checklist Template to Many Cards",
     "copyChecklistToManyCardsPopup-title": "Copy Checklist Template to Many Cards",
     "copyChecklistToManyCardsPopup-instructions": "Destination Card Titles and Descriptions in this JSON format",
     "copyChecklistToManyCardsPopup-instructions": "Destination Card Titles and Descriptions in this JSON format",

+ 4 - 0
models/boards.js

@@ -212,6 +212,10 @@ Boards.helpers({
     return this.permission === 'public';
     return this.permission === 'public';
   },
   },
 
 
+  cards() {
+    return Cards.find({ boardId: this._id, archived: false }, { sort: { title: 1 } });
+  },
+
   lists() {
   lists() {
     return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
     return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
   },
   },

+ 7 - 0
models/cards.js

@@ -133,6 +133,13 @@ Cards.attachSchema(new SimpleSchema({
     defaultValue: -1,
     defaultValue: -1,
     optional: true,
     optional: true,
   },
   },
+  type: {
+    type: String,
+  },
+  importedId: {
+    type: String,
+    optional: true,
+  },
 }));
 }));
 
 
 Cards.allow({
 Cards.allow({

+ 11 - 0
server/migrations.js

@@ -213,6 +213,17 @@ Migrations.add('add-profile-view', () => {
   });
   });
 });
 });
 
 
+Migrations.add('add-card-types', () => {
+  Cards.find().forEach((card) => {
+    Cards.direct.update(
+      { _id: card._id },
+      { $set: {
+        type: 'cardType-card',
+        importedId: null } },
+      noValidate
+    );
+  });
+
 Migrations.add('add-custom-fields-to-cards', () => {
 Migrations.add('add-custom-fields-to-cards', () => {
   Cards.update({
   Cards.update({
     customFields: {
     customFields: {