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

First swimlane draft, no functionality

Andrés Manelli 7 жил өмнө
parent
commit
690a5b9703

+ 4 - 16
client/components/boards/boardBody.jade

@@ -20,22 +20,10 @@ template(name="boardBody")
       class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
       if showOverlay.get
         .board-overlay
-      .lists.js-lists
-        if isMiniScreen
-          if currentList
-            +list(currentList)
-          else
-            each currentBoard.lists
-              +miniList(this)
-            if currentUser.isBoardMember
-              +addListForm
-        else
-          each currentBoard.lists
-            +list(this)
-            if currentCardIsInThisList
-              +cardDetails(currentCard)
-          if currentUser.isBoardMember
-            +addListForm
+      each currentBoard.swimlanes
+        +swimlane(this)
+//      if currentUser.isBoardMember
+//        +addSwimlaneForm
 
 template(name="addListForm")
   .list.js-list.list-composer.js-list-composer

+ 0 - 146
client/components/boards/boardBody.js

@@ -29,10 +29,6 @@ BlazeComponent.extendComponent({
     this.mouseHasEnterCardDetails = false;
   },
 
-  openNewListForm() {
-    this.childComponents('addListForm')[0].open();
-  },
-
   // XXX Flow components allow us to avoid creating these two setter methods by
   // exposing a public API to modify the component state. We need to investigate
   // best practices here.
@@ -47,12 +43,6 @@ BlazeComponent.extendComponent({
     });
   },
 
-  currentCardIsInThisList() {
-    const currentCard = Cards.findOne(Session.get('currentCard'));
-    const listId = this.currentData()._id;
-    return currentCard && currentCard.listId === listId;
-  },
-
   onlyShowCurrentCard() {
     return Utils.isMiniScreen() && Session.get('currentCard');
   },
@@ -66,147 +56,11 @@ BlazeComponent.extendComponent({
           this.showOverlay.set(false);
         }
       },
-
-      // Click-and-drag action
-      'mousedown .board-canvas'(evt) {
-        // Translating the board canvas using the click-and-drag action can
-        // conflict with the build-in browser mechanism to select text. We
-        // define a list of elements in which we disable the dragging because
-        // the user will legitimately expect to be able to select some text with
-        // his mouse.
-        const noDragInside = ['a', 'input', 'textarea', 'p', '.js-list-header'];
-        if ($(evt.target).closest(noDragInside.join(',')).length === 0 && $('.lists').prop('clientHeight') > evt.offsetY) {
-          this._isDragging = true;
-          this._lastDragPositionX = evt.clientX;
-        }
-      },
       'mouseup'() {
         if (this._isDragging) {
           this._isDragging = false;
         }
       },
-      'mousemove'(evt) {
-        if (this._isDragging) {
-          // Update the canvas position
-          this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
-          this._lastDragPositionX = evt.clientX;
-          // Disable browser text selection while dragging
-          evt.stopPropagation();
-          evt.preventDefault();
-          // Don't close opened card or inlined form at the end of the
-          // click-and-drag.
-          EscapeActions.executeUpTo('popup-close');
-          EscapeActions.preventNextClick();
-        }
-      },
     }];
   },
 }).register('board');
-
-Template.boardBody.onRendered(function() {
-  const self = BlazeComponent.getComponentForElement(this.firstNode);
-
-  self.listsDom = this.find('.js-lists');
-
-  if (!Session.get('currentCard')) {
-    self.scrollLeft();
-  }
-
-  // We want to animate the card details window closing. We rely on CSS
-  // transition for the actual animation.
-  self.listsDom._uihooks = {
-    removeElement(node) {
-      const removeNode = _.once(() => {
-        node.parentNode.removeChild(node);
-      });
-      if ($(node).hasClass('js-card-details')) {
-        $(node).css({
-          flexBasis: 0,
-          padding: 0,
-        });
-        $(self.listsDom).one(CSSEvents.transitionend, removeNode);
-      } else {
-        removeNode();
-      }
-    },
-  };
-
-  $(self.listsDom).sortable({
-    tolerance: 'pointer',
-    helper: 'clone',
-    handle: '.js-list-header',
-    items: '.js-list:not(.js-list-composer)',
-    placeholder: 'list placeholder',
-    distance: 7,
-    start(evt, ui) {
-      ui.placeholder.height(ui.helper.height());
-      Popup.close();
-    },
-    stop() {
-      $(self.listsDom).find('.js-list:not(.js-list-composer)').each(
-        (i, list) => {
-          const data = Blaze.getData(list);
-          Lists.update(data._id, {
-            $set: {
-              sort: i,
-            },
-          });
-        }
-      );
-    },
-  });
-
-  function userIsMember() {
-    return Meteor.user() && Meteor.user().isBoardMember();
-  }
-
-  // Disable drag-dropping while in multi-selection mode, or if the current user
-  // is not a board member
-  self.autorun(() => {
-    const $listDom = $(self.listsDom);
-    if ($listDom.data('sortable')) {
-      $(self.listsDom).sortable('option', 'disabled',
-        MultiSelection.isActive() || !userIsMember());
-    }
-  });
-
-  // If there is no data in the board (ie, no lists) we autofocus the list
-  // creation form by clicking on the corresponding element.
-  const currentBoard = Boards.findOne(Session.get('currentBoard'));
-  if (userIsMember() && currentBoard.lists().count() === 0) {
-    self.openNewListForm();
-  }
-});
-
-BlazeComponent.extendComponent({
-  // Proxy
-  open() {
-    this.childComponents('inlinedForm')[0].open();
-  },
-
-  events() {
-    return [{
-      submit(evt) {
-        evt.preventDefault();
-        const titleInput = this.find('.list-name-input');
-        const title = titleInput.value.trim();
-        if (title) {
-          Lists.insert({
-            title,
-            boardId: Session.get('currentBoard'),
-            sort: $('.list').length,
-          });
-
-          titleInput.value = '';
-          titleInput.focus();
-        }
-      },
-    }];
-  },
-}).register('addListForm');
-
-Template.boardBody.helpers({
-  canSeeAddList() {
-    return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
-  },
-});

+ 4 - 2
client/components/boards/boardBody.styl

@@ -20,7 +20,8 @@ position()
     &.is-sibling-sidebar-open
       margin-right: 248px
 
-    .lists
+    .swimlane
+      border-bottom: 1px solid #CCC
       align-items: flex-start
       display: flex
       flex-direction: row
@@ -49,8 +50,9 @@ position()
 
     .board-canvas
 
-      .lists
+      .swimlane
         align-items: flex-start
+        border-bottom: 1px solid #CCC
         display: flex
         flex-direction: column
         margin: 0

+ 5 - 0
client/components/boards/boardHeader.js

@@ -164,6 +164,11 @@ const CreateBoard = BlazeComponent.extendComponent({
       permission: visibility,
     }));
 
+    Swimlanes.insert({
+      title: 'Default',
+      boardId: this.boardId.get(),
+    });
+
     Utils.goBoardId(this.boardId.get());
   },
 

+ 6 - 6
client/components/cards/cardDetails.js

@@ -20,8 +20,8 @@ BlazeComponent.extendComponent({
 
   onCreated() {
     this.isLoaded = new ReactiveVar(false);
-    this.parentComponent().showOverlay.set(true);
-    this.parentComponent().mouseHasEnterCardDetails = false;
+    this.parentComponent().parentComponent().showOverlay.set(true);
+    this.parentComponent().parentComponent().mouseHasEnterCardDetails = false;
     this.calculateNextPeak();
 
     Meteor.subscribe('unsaved-edits');
@@ -42,7 +42,7 @@ BlazeComponent.extendComponent({
 
   scrollParentContainer() {
     const cardPanelWidth = 510;
-    const bodyBoardComponent = this.parentComponent();
+    const bodyBoardComponent = this.parentComponent().parentComponent();
 
     const $cardContainer = bodyBoardComponent.$('.js-lists');
     const $cardView = this.$(this.firstNode());
@@ -69,7 +69,7 @@ BlazeComponent.extendComponent({
   },
 
   onDestroyed() {
-    this.parentComponent().showOverlay.set(false);
+    this.parentComponent().parentComponent().showOverlay.set(false);
   },
 
   events() {
@@ -104,8 +104,8 @@ BlazeComponent.extendComponent({
       'click .js-add-members': Popup.open('cardMembers'),
       'click .js-add-labels': Popup.open('cardLabels'),
       'mouseenter .js-card-details' () {
-        this.parentComponent().showOverlay.set(true);
-        this.parentComponent().mouseHasEnterCardDetails = true;
+        this.parentComponent().parentComponent().showOverlay.set(true);
+        this.parentComponent().parentComponent().mouseHasEnterCardDetails = true;
       },
       'click #toggleButton'() {
         Meteor.call('toggleSystemMessages');

+ 1 - 1
client/components/lists/list.js

@@ -18,7 +18,7 @@ BlazeComponent.extendComponent({
   // callback, we basically solve all issues related to reactive updates. A
   // comment below provides further details.
   onRendered() {
-    const boardComponent = this.parentComponent();
+    const boardComponent = this.parentComponent().parentComponent();
     const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
     const $cards = this.$('.js-minicards');
     $cards.sortable({

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

@@ -4,7 +4,7 @@ template(name="listBody")
       if cards.count
         +inlinedForm(autoclose=false position="top")
           +addCardForm(listId=_id position="top")
-      each cards
+      each cards ../../_id
         a.minicard-wrapper.js-minicard(href=absoluteUrl
           class="{{#if cardIsSelected}}is-selected{{/if}}"
           class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")

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

@@ -36,6 +36,7 @@ BlazeComponent.extendComponent({
     const members = formComponent.members.get();
     const labelIds = formComponent.labels.get();
 
+    const swimlaneId = this.parentComponent().parentComponent().data()._id;
     if (title) {
       const _id = Cards.insert({
         title,
@@ -44,6 +45,7 @@ BlazeComponent.extendComponent({
         listId: this.data()._id,
         boardId: this.data().board()._id,
         sort: sortIndex,
+        swimlaneId: swimlaneId,
       });
       // 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

+ 20 - 0
client/components/swimlanes/swimlanes.jade

@@ -0,0 +1,20 @@
+template(name="swimlane")
+  .swimlane.js-lists
+    .swimlane-header-wrap
+      .swimlane-header
+        = title
+    if isMiniScreen
+      if currentList
+        +list(currentList)
+      else
+        each currentBoard.lists
+          +miniList(this)
+        if currentUser.isBoardMember
+          +addListForm
+    else
+      each currentBoard.lists
+        +list(this)
+        if currentCardIsInThisList
+          +cardDetails(currentCard)
+      if currentUser.isBoardMember
+        +addListForm

+ 181 - 0
client/components/swimlanes/swimlanes.js

@@ -0,0 +1,181 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.draggingActive = new ReactiveVar(false);
+
+    this._isDragging = false;
+    this._lastDragPositionX = 0;
+  },
+
+  openNewListForm() {
+    this.childComponents('addListForm')[0].open();
+  },
+
+  id() {
+      return this._id;
+  },
+
+  // XXX Flow components allow us to avoid creating these two setter methods by
+  // exposing a public API to modify the component state. We need to investigate
+  // best practices here.
+  setIsDragging(bool) {
+    this.draggingActive.set(bool);
+  },
+
+  scrollLeft(position = 0) {
+    const lists = this.$('.js-lists');
+    lists && lists.animate({
+      scrollLeft: position,
+    });
+  },
+
+  currentCardIsInThisList() {
+    const currentCard = Cards.findOne(Session.get('currentCard'));
+    const listId = this.currentData()._id;
+    return currentCard && currentCard.listId === listId; //TODO: AND IN THIS SWIMLANE
+  },
+
+  events() {
+    return [{
+      // Click-and-drag action
+      'mousedown .board-canvas'(evt) {
+        // Translating the board canvas using the click-and-drag action can
+        // conflict with the build-in browser mechanism to select text. We
+        // define a list of elements in which we disable the dragging because
+        // the user will legitimately expect to be able to select some text with
+        // his mouse.
+        const noDragInside = ['a', 'input', 'textarea', 'p', '.js-list-header'];
+        if ($(evt.target).closest(noDragInside.join(',')).length === 0 && this.$('.swimlane').prop('clientHeight') > evt.offsetY) {
+          this._isDragging = true;
+          this._lastDragPositionX = evt.clientX;
+        }
+      },
+      'mouseup'() {
+        if (this._isDragging) {
+          this._isDragging = false;
+        }
+      },
+      'mousemove'(evt) {
+        if (this._isDragging) {
+          // Update the canvas position
+          this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
+          this._lastDragPositionX = evt.clientX;
+          // Disable browser text selection while dragging
+          evt.stopPropagation();
+          evt.preventDefault();
+          // Don't close opened card or inlined form at the end of the
+          // click-and-drag.
+          EscapeActions.executeUpTo('popup-close');
+          EscapeActions.preventNextClick();
+        }
+      },
+    }];
+  },
+}).register('swimlane');
+
+Template.swimlane.onRendered(function() {
+  const self = BlazeComponent.getComponentForElement(this.firstNode);
+
+  self.listsDom = this.find('.js-lists');
+
+  if (!Session.get('currentCard')) {
+    self.scrollLeft();
+  }
+
+  // We want to animate the card details window closing. We rely on CSS
+  // transition for the actual animation.
+  self.listsDom._uihooks = {
+    removeElement(node) {
+      const removeNode = _.once(() => {
+        node.parentNode.removeChild(node);
+      });
+      if ($(node).hasClass('js-card-details')) {
+        $(node).css({
+          flexBasis: 0,
+          padding: 0,
+        });
+        $(self.listsDom).one(CSSEvents.transitionend, removeNode);
+      } else {
+        removeNode();
+      }
+    },
+  };
+
+  $(self.listsDom).sortable({
+    tolerance: 'pointer',
+    helper: 'clone',
+    handle: '.js-list-header',
+    items: '.js-list:not(.js-list-composer)',
+    placeholder: 'list placeholder',
+    distance: 7,
+    start(evt, ui) {
+      ui.placeholder.height(ui.helper.height());
+      Popup.close();
+    },
+    stop() {
+      $(self.listsDom).find('.js-list:not(.js-list-composer)').each(
+        (i, list) => {
+          const data = Blaze.getData(list);
+          Lists.update(data._id, {
+            $set: {
+              sort: i,
+            },
+          });
+        }
+      );
+    },
+  });
+
+  function userIsMember() {
+    return Meteor.user() && Meteor.user().isBoardMember();
+  }
+
+  // Disable drag-dropping while in multi-selection mode, or if the current user
+  // is not a board member
+  self.autorun(() => {
+    const $listDom = $(self.listsDom);
+    if ($listDom.data('sortable')) {
+      $(self.listsDom).sortable('option', 'disabled',
+        MultiSelection.isActive() || !userIsMember());
+    }
+  });
+
+  // If there is no data in the board (ie, no lists) we autofocus the list
+  // creation form by clicking on the corresponding element.
+  const currentBoard = Boards.findOne(Session.get('currentBoard'));
+  if (userIsMember() && currentBoard.lists().count() === 0) {
+    self.openNewListForm();
+  }
+});
+
+BlazeComponent.extendComponent({
+  // Proxy
+  open() {
+    this.childComponents('inlinedForm')[0].open();
+  },
+
+  events() {
+    return [{
+      submit(evt) {
+        evt.preventDefault();
+        const titleInput = this.find('.list-name-input');
+        const title = titleInput.value.trim();
+        if (title) {
+          Lists.insert({
+            title,
+            boardId: Session.get('currentBoard'),
+            sort: $('.list').length,
+          });
+
+          titleInput.value = '';
+          titleInput.focus();
+        }
+      },
+    }];
+  },
+}).register('addListForm');
+
+Template.swimlane.helpers({
+  canSeeAddList() {
+    return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+  },
+});

+ 21 - 0
client/components/swimlanes/swimlanes.styl

@@ -0,0 +1,21 @@
+@import 'nib'
+
+.swimlane-header-wrap
+  display: flex;
+  flex-direction: column;
+  flex: 0 0 50px;
+
+  .swimlane-header
+    writing-mode: sideways-lr;
+    height: 100%;
+    font-size: 14px;
+    line-height: 50px;
+    margin: 0;
+    font-weight: bold;
+    min-height: 9px;
+    min-width: 30px;
+    overflow: hidden;
+    -o-text-overflow: ellipsis;
+    text-overflow: ellipsis;
+    word-wrap: break-word;
+    text-align: center;

+ 4 - 0
models/boards.js

@@ -187,6 +187,10 @@ Boards.helpers({
     return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
   },
 
+  swimlanes() {
+    return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
+  },
+
   hasOvertimeCards(){
     const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} );
     return card !== undefined;

+ 3 - 0
models/cards.js

@@ -18,6 +18,9 @@ Cards.attachSchema(new SimpleSchema({
   listId: {
     type: String,
   },
+  swimlaneId: {
+    type: String,
+  },
   // The system could work without this `boardId` information (we could deduce
   // the board identifier from the card), but it would make the system more
   // difficult to manage and less efficient.

+ 2 - 1
models/lists.js

@@ -75,10 +75,11 @@ Lists.allow({
 });
 
 Lists.helpers({
-  cards() {
+  cards(swimlaneId) {
     return Cards.find(Filter.mongoSelector({
       listId: this._id,
       archived: false,
+      swimlaneId: swimlaneId,
     }), { sort: ['sort'] });
   },
 

+ 219 - 0
models/swimlanes.js

@@ -0,0 +1,219 @@
+Swimlanes = new Mongo.Collection('swimlanes');
+
+Swimlanes.attachSchema(new SimpleSchema({
+  title: {
+    type: String,
+  },
+  archived: {
+    type: Boolean,
+    autoValue() { // eslint-disable-line consistent-return
+      if (this.isInsert && !this.isSet) {
+        return false;
+      }
+    },
+  },
+  boardId: {
+    type: String,
+  },
+  createdAt: {
+    type: Date,
+    autoValue() { // eslint-disable-line consistent-return
+      if (this.isInsert) {
+        return new Date();
+      } else {
+        this.unset();
+      }
+    },
+  },
+  sort: {
+    type: Number,
+    decimal: true,
+    // XXX We should probably provide a default
+    optional: true,
+  },
+  updatedAt: {
+    type: Date,
+    optional: true,
+    autoValue() { // eslint-disable-line consistent-return
+      if (this.isUpdate) {
+        return new Date();
+      } else {
+        this.unset();
+      }
+    },
+  },
+}));
+
+Swimlanes.allow({
+  insert(userId, doc) {
+    return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId));
+  },
+  update(userId, doc) {
+    return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId));
+  },
+  remove(userId, doc) {
+    return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId));
+  },
+  fetch: ['boardId'],
+});
+
+Swimlanes.helpers({
+  cards() {
+    return Cards.find(Filter.mongoSelector({
+      swimlaneId: this._id,
+      archived: false,
+    }), { sort: ['sort'] });
+  },
+
+  allCards() {
+    return Cards.find({ swimlaneId: this._id });
+  },
+
+  board() {
+    return Boards.findOne(this.boardId);
+  },
+});
+
+Swimlanes.mutations({
+  rename(title) {
+    return { $set: { title } };
+  },
+
+  archive() {
+    return { $set: { archived: true } };
+  },
+
+  restore() {
+    return { $set: { archived: false } };
+  },
+});
+
+Swimlanes.hookOptions.after.update = { fetchPrevious: false };
+
+if (Meteor.isServer) {
+  Meteor.startup(() => {
+    Swimlanes._collection._ensureIndex({ boardId: 1 });
+  });
+
+  Swimlanes.after.insert((userId, doc) => {
+    Activities.insert({
+      userId,
+      type: 'swimlane',
+      activityType: 'createSwimlane',
+      boardId: doc.boardId,
+      swimlaneId: doc._id,
+    });
+  });
+
+  Swimlanes.before.remove((userId, doc) => {
+    Activities.insert({
+      userId,
+      type: 'swimlane',
+      activityType: 'removeSwimlane',
+      boardId: doc.boardId,
+      swimlaneId: doc._id,
+      title: doc.title,
+    });
+  });
+
+  Swimlanes.after.update((userId, doc) => {
+    if (doc.archived) {
+      Activities.insert({
+        userId,
+        type: 'swimlane',
+        activityType: 'archivedSwimlane',
+        swimlaneId: doc._id,
+        boardId: doc.boardId,
+      });
+    }
+  });
+}
+
+//SWIMLANE REST API
+if (Meteor.isServer) {
+  JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res, next) {
+    try {
+      const paramBoardId = req.params.boardId;
+      Authentication.checkBoardAccess( req.userId, paramBoardId);
+
+      JsonRoutes.sendResult(res, {
+        code: 200,
+        data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(function (doc) {
+          return {
+            _id: doc._id,
+            title: doc.title,
+          };
+        }),
+      });
+    }
+    catch (error) {
+      JsonRoutes.sendResult(res, {
+        code: 200,
+        data: error,
+      });
+    }
+  });
+
+  JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res, next) {
+    try {
+      const paramBoardId = req.params.boardId;
+      const paramSwimlaneId = req.params.swimlaneId;
+      Authentication.checkBoardAccess( req.userId, paramBoardId);
+      JsonRoutes.sendResult(res, {
+        code: 200,
+        data: Swimlanes.findOne({ _id: paramSwimlaneId, boardId: paramBoardId, archived: false }),
+      });
+    }
+    catch (error) {
+      JsonRoutes.sendResult(res, {
+        code: 200,
+        data: error,
+      });
+    }
+  });
+
+  JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function (req, res, next) {
+    try {
+      Authentication.checkUserId( req.userId);
+      const paramBoardId = req.params.boardId;
+      const id = Swimlanes.insert({
+        title: req.body.title,
+        boardId: paramBoardId,
+      });
+      JsonRoutes.sendResult(res, {
+        code: 200,
+        data: {
+          _id: id,
+        },
+      });
+    }
+    catch (error) {
+      JsonRoutes.sendResult(res, {
+        code: 200,
+        data: error,
+      });
+    }
+  });
+
+  JsonRoutes.add('DELETE', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res, next) {
+    try {
+      Authentication.checkUserId( req.userId);
+      const paramBoardId = req.params.boardId;
+      const paramSwimlaneId = req.params.swimlaneId;
+      Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
+      JsonRoutes.sendResult(res, {
+        code: 200,
+        data: {
+          _id: paramSwimlaneId,
+        },
+      });
+    }
+    catch (error) {
+      JsonRoutes.sendResult(res, {
+        code: 200,
+        data: error,
+      });
+    }
+  });
+
+}

+ 1 - 0
server/publications/boards.js

@@ -73,6 +73,7 @@ Meteor.publishRelations('board', function(boardId) {
     ],
   }, { limit: 1 }), function(boardId, board) {
     this.cursor(Lists.find({ boardId }));
+    this.cursor(Swimlanes.find({ boardId }));
     this.cursor(Integrations.find({ boardId }));
 
     // Cards and cards comments