瀏覽代碼

Merge branch 'andresmanelli-checklistItems' into devel

Lauri Ojansivu 7 年之前
父節點
當前提交
a7855f3f58

+ 1 - 0
.eslintrc.json

@@ -131,6 +131,7 @@
     "AccountSettings": true,
     "Announcements": true,
     "Swimlanes": true,
+    "ChecklistItems": true,
     "Npm": true
   }
 }

+ 8 - 0
CHANGELOG.md

@@ -1,3 +1,11 @@
+# Upcoming Wekan release
+
+This release adds the following new features:
+
+- [Checklist items sort fix, and checklist sort capability](https://github.com/wekan/wekan/pull/1543).
+
+Thanks to GitHub user andresmanelli for contributions.
+
 # v0.78 2018-03-17 Wekan release
 
 This release adds the following new features:

+ 46 - 0
client/components/cards/cardDetails.js

@@ -1,4 +1,5 @@
 const subManager = new SubsManager();
+const { calculateIndexData } = Utils;
 
 BlazeComponent.extendComponent({
   mixins() {
@@ -66,6 +67,51 @@ BlazeComponent.extendComponent({
 
   onRendered() {
     if (!Utils.isMiniScreen()) this.scrollParentContainer();
+    const $checklistsDom = this.$('.card-checklist-items');
+
+    $checklistsDom.sortable({
+      tolerance: 'pointer',
+      helper: 'clone',
+      handle: '.checklist-title',
+      items: '.js-checklist',
+      placeholder: 'checklist placeholder',
+      distance: 7,
+      start(evt, ui) {
+        ui.placeholder.height(ui.helper.height());
+        EscapeActions.executeUpTo('popup-close');
+      },
+      stop(evt, ui) {
+        let prevChecklist = ui.item.prev('.js-checklist').get(0);
+        if (prevChecklist) {
+          prevChecklist = Blaze.getData(prevChecklist).checklist;
+        }
+        let nextChecklist = ui.item.next('.js-checklist').get(0);
+        if (nextChecklist) {
+          nextChecklist = Blaze.getData(nextChecklist).checklist;
+        }
+        const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
+
+        $checklistsDom.sortable('cancel');
+        const checklist = Blaze.getData(ui.item.get(0)).checklist;
+
+        Checklists.update(checklist._id, {
+          $set: {
+            sort: sortIndex.base,
+          },
+        });
+      },
+    });
+
+    function userIsMember() {
+      return Meteor.user() && Meteor.user().isBoardMember();
+    }
+
+    // Disable sorting if the current user is not a board member
+    this.autorun(() => {
+      if ($checklistsDom.data('sortable')) {
+        $checklistsDom.sortable('option', 'disabled', !userIsMember());
+      }
+    });
   },
 
   onDestroyed() {

+ 20 - 19
client/components/cards/checklists.jade

@@ -18,24 +18,25 @@ template(name="checklists")
         | {{_ 'add-checklist'}}...
 
 template(name="checklistDetail")
-  +inlinedForm(classNames="js-edit-checklist-title" checklist = checklist)
-    +editChecklistItemForm(checklist = checklist)
-  else
-    .checklist-title
-      .checkbox.fa.fa-check-square-o
-      if canModifyCard
-        a.js-delete-checklist.toggle-delete-checklist-dialog {{_ "delete"}}...
-
-      span.checklist-stat(class="{{#if checklist.isFinished}}is-finished{{/if}}") {{checklist.finishedCount}}/{{checklist.itemCount}}
-      if canModifyCard
-        h2.title.js-open-inlined-form.is-editable
-          +viewer
-            = checklist.title
-      else
-        h2.title
-          +viewer
+  .js-checklist.checklist
+    +inlinedForm(classNames="js-edit-checklist-title" checklist = checklist)
+      +editChecklistItemForm(checklist = checklist)
+    else
+      .checklist-title
+        .checkbox.fa.fa-check-square-o
+        if canModifyCard
+          a.js-delete-checklist.toggle-delete-checklist-dialog {{_ "delete"}}...
+  
+        span.checklist-stat(class="{{#if checklist.isFinished}}is-finished{{/if}}") {{checklist.finishedCount}}/{{checklist.itemCount}}
+        if canModifyCard
+          h2.title.js-open-inlined-form.is-editable
+            +viewer
               = checklist.title
-  +checklistItems(checklist = checklist)
+        else
+          h2.title
+            +viewer
+                = checklist.title
+    +checklistItems(checklist = checklist)
 
 template(name="checklistDeleteDialog")
   .js-confirm-checklist-delete
@@ -70,7 +71,7 @@ template(name="editChecklistItemForm")
 
 template(name="checklistItems")
   .checklist-items.js-checklist-items
-    each item in checklist.getItemsSorted
+    each item in checklist.items
       +inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
         +editChecklistItemForm(type = 'item' item = item checklist = checklist)
       else
@@ -84,7 +85,7 @@ template(name="checklistItems")
           | {{_ 'add-checklist-item'}}...
 
 template(name='itemDetail')
-  .item.js-checklist-item
+  .js-checklist-item.checklist-item
     if canModifyCard
       .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
       .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")

+ 57 - 53
client/components/cards/checklists.js

@@ -1,11 +1,14 @@
+const { calculateIndexData } = Utils;
+
 function initSorting(items) {
   items.sortable({
     tolerance: 'pointer',
     helper: 'clone',
     items: '.js-checklist-item:not(.placeholder)',
-    axis: 'y',
+    connectWith: '.js-checklist-items',
+    appendTo: '.board-canvas',
     distance: 7,
-    placeholder: 'placeholder',
+    placeholder: 'checklist-item placeholder',
     scroll: false,
     start(evt, ui) {
       ui.placeholder.height(ui.helper.height());
@@ -13,57 +16,54 @@ function initSorting(items) {
     },
     stop(evt, ui) {
       const parent = ui.item.parents('.js-checklist-items');
-      const orderedItems = [];
-      parent.find('.js-checklist-item').each(function(i, item) {
-        const checklistItem = Blaze.getData(item).item;
-        orderedItems.push(checklistItem._id);
-      });
-      items.sortable('cancel');
-      const formerParent = ui.item.parents('.js-checklist-items');
-      const checklist = Blaze.getData(parent.get(0)).checklist;
-      const oldChecklist = Blaze.getData(formerParent.get(0)).checklist;
-      if (oldChecklist._id !== checklist._id) {
-        const currentItem = Blaze.getData(ui.item.get(0)).item;
-        for (let i = 0; i < orderedItems.length; i++) {
-          const itemId = orderedItems[i];
-          if (itemId !== currentItem._id) continue;
-          const newItem = {
-            _id: checklist.getNewItemId(),
-            title: currentItem.title,
-            sort: i,
-            isFinished: currentItem.isFinished,
-          };
-          checklist.addFullItem(newItem);
-          orderedItems[i] = currentItem._id;
-          oldChecklist.removeItem(itemId);
-        }
-      } else {
-        checklist.sortItems(orderedItems);
+      const checklistId = Blaze.getData(parent.get(0)).checklist._id;
+      let prevItem = ui.item.prev('.js-checklist-item').get(0);
+      if (prevItem) {
+        prevItem = Blaze.getData(prevItem).item;
+      }
+      let nextItem = ui.item.next('.js-checklist-item').get(0);
+      if (nextItem) {
+        nextItem = Blaze.getData(nextItem).item;
       }
+      const nItems = 1;
+      const sortIndex = calculateIndexData(prevItem, nextItem, nItems);
+      const checklistDomElement = ui.item.get(0);
+      const checklistData = Blaze.getData(checklistDomElement);
+      const checklistItem = checklistData.item;
+
+      items.sortable('cancel');
+
+      checklistItem.move(checklistId, sortIndex.base);
     },
   });
 }
 
-Template.checklists.onRendered(function () {
-  const self = BlazeComponent.getComponentForElement(this.firstNode);
-  self.itemsDom = this.$('.card-checklist-items');
-  initSorting(self.itemsDom);
-  self.itemsDom.mousedown(function(evt) {
-    evt.stopPropagation();
-  });
+BlazeComponent.extendComponent({
+  onRendered() {
+    const self = this;
+    self.itemsDom = this.$('.js-checklist-items');
+    initSorting(self.itemsDom);
+    self.itemsDom.mousedown(function(evt) {
+      evt.stopPropagation();
+    });
+
+    function userIsMember() {
+      return Meteor.user() && Meteor.user().isBoardMember();
+    }
 
-  function userIsMember() {
-    return Meteor.user() && Meteor.user().isBoardMember();
-  }
+    // Disable sorting if the current user is not a board member
+    self.autorun(() => {
+      const $itemsDom = $(self.itemsDom);
+      if ($itemsDom.data('sortable')) {
+        $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
+      }
+    });
+  },
 
-  // Disable sorting if the current user is not a board member
-  self.autorun(() => {
-    const $itemsDom = $(self.itemsDom);
-    if ($itemsDom.data('sortable')) {
-      $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
-    }
-  });
-});
+  canModifyCard() {
+    return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+  },
+}).register('checklistDetail');
 
 BlazeComponent.extendComponent({
 
@@ -95,7 +95,12 @@ BlazeComponent.extendComponent({
     const checklist = this.currentData().checklist;
 
     if (title) {
-      checklist.addItem(title);
+      ChecklistItems.insert({
+        title,
+        checklistId: checklist._id,
+        cardId: checklist.cardId,
+        sort: checklist.itemCount(),
+      });
     }
     // We keep the form opened, empty it.
     textarea.value = '';
@@ -118,7 +123,7 @@ BlazeComponent.extendComponent({
     const checklist = this.currentData().checklist;
     const item = this.currentData().item;
     if (checklist && item && item._id) {
-      checklist.removeItem(item._id);
+      ChecklistItems.remove(item._id);
     }
   },
 
@@ -135,9 +140,8 @@ BlazeComponent.extendComponent({
 
     const textarea = this.find('textarea.js-edit-checklist-item');
     const title = textarea.value.trim();
-    const itemId = this.currentData().item._id;
-    const checklist = this.currentData().checklist;
-    checklist.editItem(itemId, title);
+    const item = this.currentData().item;
+    item.setTitle(title);
   },
 
   onCreated() {
@@ -211,12 +215,12 @@ BlazeComponent.extendComponent({
     const checklist = this.currentData().checklist;
     const item = this.currentData().item;
     if (checklist && item && item._id) {
-      checklist.toggleItem(item._id);
+      item.toggleItem();
     }
   },
   events() {
     return [{
-      'click .item .check-box': this.toggleItem,
+      'click .js-checklist-item .check-box': this.toggleItem,
     }];
   },
 }).register('itemDetail');

+ 54 - 28
client/components/cards/checklists.styl

@@ -78,34 +78,60 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
   bottom: -600px
   right: 0
 
-.checklist-items
+.checklist
+  background: darken(white, 3%)
+
+  &.placeholder
+    background: darken(white, 20%)
+    border-radius: 2px
+
+  &.ui-sortable-helper
+    box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
+                0 0 1px rgba(0, 0, 0, .5)
+    transform: rotate(4deg)
+    cursor: grabbing
+
+
+.checklist-item
   margin: 0 0 0.5em 1.33em
+  line-height: 25px
+  font-size: 1.1em
+  margin-top: 3px
+  display: flex
+  background: darken(white, 3%)
+
+  &.placeholder
+    background: darken(white, 20%)
+    border-radius: 2px
+
+  &.ui-sortable-helper
+    box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
+                0 0 1px rgba(0, 0, 0, .5)
+    transform: rotate(4deg)
+    cursor: grabbing
 
-  .item
-    line-height: 25px
-    font-size: 1.1em
-    margin-top: 3px
-    display: flex
-    &:hover
-      background-color: darken(white, 8%)
-
-    .check-box
-      margin-top: 5px
-      &.is-checked
-        border-bottom: 2px solid #3cb500
-        border-right: 2px solid #3cb500
-
-    .item-title
-      flex: 1
-      padding-left: 10px;
-      &.is-checked
-        color: #8c8c8c
-        font-style: italic
-
-  .js-delete-checklist-item
-    @extends .delete-text
-    padding: 12px 0 0 0
+  &:hover
+    background-color: darken(white, 8%)
+
+  .check-box
+    margin-top: 5px
+    &.is-checked
+      border-bottom: 2px solid #3cb500
+      border-right: 2px solid #3cb500
+
+  .item-title
+    flex: 1
+    padding-left: 10px;
+    &.is-checked
+      color: #8c8c8c
+      font-style: italic
+
+.js-delete-checklist-item
+  margin: 0 0 0.5em 1.33em
+  @extends .delete-text
+  padding: 12px 0 0 0
 
-  .add-checklist-item
-    padding-top: 0.5em
-    display: inline-block
+.add-checklist-item
+  margin: 0 0 0.5em 1.33em
+  padding-top: 0.5em
+  display: inline-block

+ 31 - 0
client/lib/utils.js

@@ -33,6 +33,37 @@ Utils = {
     return $(window).width() <= 800;
   },
 
+  calculateIndexData(prevData, nextData, nItems = 1) {
+    let base, increment;
+    // If we drop the card to an empty column
+    if (!prevData && !nextData) {
+      base = 0;
+      increment = 1;
+    // If we drop the card in the first position
+    } else if (!prevData) {
+      base = nextData.sort - 1;
+      increment = -1;
+    // If we drop the card in the last position
+    } else if (!nextData) {
+      base = prevData.sort + 1;
+      increment = 1;
+    }
+    // In the general case take the average of the previous and next element
+    // sort indexes.
+    else {
+      const prevSortIndex = prevData.sort;
+      const nextSortIndex = nextData.sort;
+      increment = (nextSortIndex - prevSortIndex) / (nItems + 1);
+      base = prevSortIndex + increment;
+    }
+    // XXX Return a generator that yield values instead of a base with a
+    // increment number.
+    return {
+      base,
+      increment,
+    };
+  },
+
   // Determine the new sort index
   calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) {
     let base, increment;

+ 1 - 1
models/activities.js

@@ -42,7 +42,7 @@ Activities.helpers({
     return Checklists.findOne(this.checklistId);
   },
   checklistItem() {
-    return Checklists.findOne(this.checklistId).getItem(this.checklistItemId);
+    return ChecklistItems.findOne(this.checklistItemId);
   },
 });
 

+ 1 - 1
models/cards.js

@@ -155,7 +155,7 @@ Cards.helpers({
   },
 
   checklists() {
-    return Checklists.find({cardId: this._id}, {sort: {createdAt: 1}});
+    return Checklists.find({cardId: this._id}, {sort: { sort: 1 } });
   },
 
   checklistItemCount() {

+ 95 - 0
models/checklistItems.js

@@ -0,0 +1,95 @@
+ChecklistItems = new Mongo.Collection('checklistItems');
+
+ChecklistItems.attachSchema(new SimpleSchema({
+  title: {
+    type: String,
+  },
+  sort: {
+    type: Number,
+    decimal: true,
+  },
+  isFinished: {
+    type: Boolean,
+    defaultValue: false,
+  },
+  checklistId: {
+    type: String,
+  },
+  cardId: {
+    type: String,
+  },
+}));
+
+ChecklistItems.allow({
+  insert(userId, doc) {
+    return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId));
+  },
+  update(userId, doc) {
+    return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId));
+  },
+  remove(userId, doc) {
+    return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId));
+  },
+  fetch: ['userId', 'cardId'],
+});
+
+ChecklistItems.before.insert((userId, doc) => {
+  if (!doc.userId) {
+    doc.userId = userId;
+  }
+});
+
+// Mutations
+ChecklistItems.mutations({
+  setTitle(title) {
+    return { $set: { title } };
+  },
+  toggleItem() {
+    return { $set: { isFinished: !this.isFinished } };
+  },
+  move(checklistId, sortIndex) {
+    const cardId = Checklists.findOne(checklistId).cardId;
+    const mutatedFields = {
+      cardId,
+      checklistId,
+      sort: sortIndex,
+    };
+
+    return {$set: mutatedFields};
+  },
+});
+
+// Activities helper
+function itemCreation(userId, doc) {
+  const card = Cards.findOne(doc.cardId);
+  const boardId = card.boardId;
+  Activities.insert({
+    userId,
+    activityType: 'addChecklistItem',
+    cardId: doc.cardId,
+    boardId,
+    checklistId: doc.checklistId,
+    checklistItemId: doc._id,
+  });
+}
+
+function itemRemover(userId, doc) {
+  Activities.remove({
+    checklistItemId: doc._id,
+  });
+}
+
+// Activities
+if (Meteor.isServer) {
+  Meteor.startup(() => {
+    ChecklistItems._collection._ensureIndex({ checklistId: 1 });
+  });
+
+  ChecklistItems.after.insert((userId, doc) => {
+    itemCreation(userId, doc);
+  });
+
+  ChecklistItems.after.remove((userId, doc) => {
+    itemRemover(userId, doc);
+  });
+}

+ 9 - 163
models/checklists.js

@@ -7,24 +7,6 @@ Checklists.attachSchema(new SimpleSchema({
   title: {
     type: String,
   },
-  items: {
-    type: [Object],
-    defaultValue: [],
-  },
-  'items.$._id': {
-    type: String,
-  },
-  'items.$.title': {
-    type: String,
-  },
-  'items.$.sort': {
-    type: Number,
-    decimal: true,
-  },
-  'items.$.isFinished': {
-    type: Boolean,
-    defaultValue: false,
-  },
   finishedAt: {
     type: Date,
     optional: true,
@@ -46,40 +28,28 @@ Checklists.attachSchema(new SimpleSchema({
   },
 }));
 
-const self = Checklists;
-
 Checklists.helpers({
   itemCount() {
-    return this.items.length;
+    return ChecklistItems.find({ checklistId: this._id }).count();
   },
-  getItemsSorted() {
-    return _.sortBy(this.items, 'sort');
+  items() {
+    return ChecklistItems.find(Filter.mongoSelector({
+      checklistId: this._id,
+    }), { sort: ['sort'] });
   },
   finishedCount() {
-    return this.items.filter((item) => {
-      return item.isFinished;
-    }).length;
+    return ChecklistItems.find({
+      checklistId: this._id,
+      isFinished: true,
+    }).count();
   },
   isFinished() {
     return 0 !== this.itemCount() && this.itemCount() === this.finishedCount();
   },
-  getItem(_id) {
-    return _.findWhere(this.items, { _id });
-  },
   itemIndex(itemId) {
     const items = self.findOne({_id : this._id}).items;
     return _.pluck(items, '_id').indexOf(itemId);
   },
-  getNewItemId() {
-    const itemCount = this.itemCount();
-    let idx = 0;
-    if (itemCount > 0) {
-      const lastId = this.items[itemCount - 1]._id;
-      const lastIdSuffix = lastId.substr(this._id.length);
-      idx = parseInt(lastIdSuffix, 10) + 1;
-    }
-    return `${this._id}${idx}`;
-  },
 });
 
 Checklists.allow({
@@ -103,108 +73,9 @@ Checklists.before.insert((userId, doc) => {
 });
 
 Checklists.mutations({
-  //for checklist itself
   setTitle(title) {
     return { $set: { title } };
   },
-  //for items in checklist
-  addItem(title) {
-    const _id = this.getNewItemId();
-    return {
-      $addToSet: {
-        items: {
-          _id, title,
-          isFinished: false,
-          sort: this.itemCount(),
-        },
-      },
-    };
-  },
-  addFullItem(item) {
-    const itemsUpdate = {};
-    this.items.forEach(function(iterItem, index) {
-      if (iterItem.sort >= item.sort) {
-        itemsUpdate[`items.${index}.sort`] = iterItem.sort + 1;
-      }
-    });
-    if (!_.isEmpty(itemsUpdate)) {
-      self.direct.update({ _id: this._id }, { $set: itemsUpdate });
-    }
-    return { $addToSet: { items: item } };
-  },
-  removeItem(itemId) {
-    const item = this.getItem(itemId);
-    const itemsUpdate = {};
-    this.items.forEach(function(iterItem, index) {
-      if (iterItem.sort > item.sort) {
-        itemsUpdate[`items.${index}.sort`] = iterItem.sort - 1;
-      }
-    });
-    if (!_.isEmpty(itemsUpdate)) {
-      self.direct.update({ _id: this._id }, { $set: itemsUpdate });
-    }
-    return { $pull: { items: { _id: itemId } } };
-  },
-  editItem(itemId, title) {
-    if (this.getItem(itemId)) {
-      const itemIndex = this.itemIndex(itemId);
-      return {
-        $set: {
-          [`items.${itemIndex}.title`]: title,
-        },
-      };
-    }
-    return {};
-  },
-  finishItem(itemId) {
-    if (this.getItem(itemId)) {
-      const itemIndex = this.itemIndex(itemId);
-      return {
-        $set: {
-          [`items.${itemIndex}.isFinished`]: true,
-        },
-      };
-    }
-    return {};
-  },
-  resumeItem(itemId) {
-    if (this.getItem(itemId)) {
-      const itemIndex = this.itemIndex(itemId);
-      return {
-        $set: {
-          [`items.${itemIndex}.isFinished`]: false,
-        },
-      };
-    }
-    return {};
-  },
-  toggleItem(itemId) {
-    const item = this.getItem(itemId);
-    if (item) {
-      const itemIndex = this.itemIndex(itemId);
-      return {
-        $set: {
-          [`items.${itemIndex}.isFinished`]: !item.isFinished,
-        },
-      };
-    }
-    return {};
-  },
-  sortItems(itemIDs) {
-    const validItems = [];
-    itemIDs.forEach((itemID) => {
-      if (this.getItem(itemID)) {
-        validItems.push(this.itemIndex(itemID));
-      }
-    });
-    const modifiedValues = {};
-    for (let i = 0; i < validItems.length; i++) {
-      modifiedValues[`items.${validItems[i]}.sort`] = i;
-    }
-    return {
-      $set: modifiedValues,
-    };
-  },
 });
 
 if (Meteor.isServer) {
@@ -222,30 +93,6 @@ if (Meteor.isServer) {
     });
   });
 
-  //TODO: so there will be no activity for adding item into checklist, maybe will be implemented in the future.
-  // The future is now
-  Checklists.after.update((userId, doc, fieldNames, modifier) => {
-    if (fieldNames.includes('items')) {
-      if (modifier.$addToSet) {
-        Activities.insert({
-          userId,
-          activityType: 'addChecklistItem',
-          cardId: doc.cardId,
-          boardId: Cards.findOne(doc.cardId).boardId,
-          checklistId: doc._id,
-          checklistItemId: modifier.$addToSet.items._id,
-        });
-      } else if (modifier.$pull) {
-        const activity = Activities.findOne({
-          checklistItemId: modifier.$pull.items._id,
-        });
-        if (activity) {
-          Activities.remove(activity._id);
-        }
-      }
-    }
-  });
-
   Checklists.before.remove((userId, doc) => {
     const activities = Activities.find({ checklistId: doc._id });
     if (activities) {
@@ -256,7 +103,6 @@ if (Meteor.isServer) {
   });
 }
 
-//CARD COMMENT REST API
 if (Meteor.isServer) {
   JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
     try {

+ 21 - 0
server/migrations.js

@@ -187,3 +187,24 @@ Migrations.add('add-views', () => {
     }
   });
 });
+
+Migrations.add('add-checklist-items', () => {
+  Checklists.find().forEach((checklist) => {
+    // Create new items
+    _.sortBy(checklist.items, 'sort').forEach((item, index) => {
+      ChecklistItems.direct.insert({
+        title: item.title,
+        sort: index,
+        isFinished: item.isFinished,
+        checklistId: checklist._id,
+        cardId: checklist.cardId,
+      });
+    });
+
+    // Delete old ones
+    Checklists.direct.update({ _id: checklist._id },
+      { $unset: { items : 1 } },
+      noValidate
+    );
+  });
+});

+ 1 - 0
server/publications/boards.js

@@ -101,6 +101,7 @@ Meteor.publishRelations('board', function(boardId) {
       this.cursor(CardComments.find({ cardId }));
       this.cursor(Attachments.find({ cardId }));
       this.cursor(Checklists.find({ cardId }));
+      this.cursor(ChecklistItems.find({ cardId }));
     });
 
     if (board.members) {