summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.json4
-rw-r--r--client/components/activities/activities.jade11
-rw-r--r--client/components/activities/activities.styl8
-rw-r--r--client/components/cards/cardDetails.jade4
-rw-r--r--client/components/cards/checklists.jade61
-rw-r--r--client/components/cards/checklists.js74
-rw-r--r--client/components/cards/checklists.styl68
-rw-r--r--client/components/cards/minicard.jade7
-rw-r--r--client/components/cards/minicard.styl16
-rw-r--r--i18n/en.i18n.json4
-rw-r--r--models/activities.js7
-rw-r--r--models/cards.js30
-rw-r--r--models/checklists.js164
-rw-r--r--server/lib/utils.js5
-rw-r--r--server/publications/boards.js1
15 files changed, 462 insertions, 2 deletions
diff --git a/.eslintrc.json b/.eslintrc.json
index 87c2e2cf..4808d873 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -117,6 +117,8 @@
"Notifications": true,
"allowIsBoardAdmin": true,
"allowIsBoardMember": true,
- "Emoji": true
+ "allowIsBoardMemberByCard": true,
+ "Emoji": true,
+ "Checklists": true
}
}
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade
index 9ff73864..9bbcd055 100644
--- a/client/components/activities/activities.jade
+++ b/client/components/activities/activities.jade
@@ -26,6 +26,12 @@ template(name="boardActivities")
+viewer
= comment.text
+ if($eq activityType 'addChecklist')
+ | {{{_ 'activity-checklist-added' cardLink}}}.
+ .activity-checklist(href="{{ card.absoluteUrl }}")
+ +viewer
+ = checklist.title
+
if($eq activityType 'archivedCard')
| {{{_ 'activity-archived' cardLink}}}.
@@ -103,6 +109,11 @@ template(name="cardActivities")
| {{{_ 'activity-attached' attachmentLink cardLabel}}}.
if attachment.isImage
img.attachment-image-preview(src=attachment.url)
+ if($eq activityType 'addChecklist')
+ | {{{_ 'activity-checklist-added' cardLabel}}}.
+ .activity-checklist
+ +viewer
+ = checklist.title
if($eq activityType 'addComment')
+inlinedForm(classNames='js-edit-comment')
diff --git a/client/components/activities/activities.styl b/client/components/activities/activities.styl
index 1f0494c7..2285fc0a 100644
--- a/client/components/activities/activities.styl
+++ b/client/components/activities/activities.styl
@@ -26,6 +26,14 @@
margin-top: 5px
padding: 5px
+ .activity-checklist
+ display: block
+ border-radius: 3px
+ background: white
+ text-decoration: none
+ box-shadow: 0 1px 2px rgba(0,0,0,.2)
+ margin-top: 5px
+ padding: 5px
.activity-meta
font-size: 0.8em
color: darken(white, 40%)
diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade
index f4212d83..cf113951 100644
--- a/client/components/cards/cardDetails.jade
+++ b/client/components/cards/cardDetails.jade
@@ -72,6 +72,10 @@ template(name="cardDetails")
h3.card-details-item-title {{_ 'description'}}
+viewer
= description
+
+ hr
+ +checklists(cardId = _id)
+
if attachments.count
hr
h2
diff --git a/client/components/cards/checklists.jade b/client/components/cards/checklists.jade
new file mode 100644
index 00000000..396cb107
--- /dev/null
+++ b/client/components/cards/checklists.jade
@@ -0,0 +1,61 @@
+template(name="checklists")
+ h2 {{_ 'checklists'}}
+ .card-checklist-items
+ each checklist in currentCard.checklists
+ +checklistDetail(checklist = checklist)
+ +inlinedForm(classNames="js-add-checklist" cardId = cardId)
+ +addChecklistItemForm
+ else
+ a.js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ 'add-checklist'}}...
+
+template(name="checklistDetail")
+ +inlinedForm(classNames="js-edit-checklist-title")
+ +editChecklistItemForm(checklist = checklist)
+ else
+ .checklist-title
+ .checkbox.fa.fa-check-square-o
+ a.js-delete-checklist {{_ "delete"}}...
+ span.checklist-stat(class="{{#if checklist.isFinished}}is-finished{{/if}}") {{checklist.finishedCount}}/{{checklist.itemCount}}
+ h2.title.js-open-inlined-form.is-editable {{checklist.title}}
+ +checklistItems(checklist = checklist)
+
+template(name="addChecklistItemForm")
+ textarea.js-add-checklist-item(rows='1' autofocus)
+ .edit-controls.clearfix
+ button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+
+template(name="editChecklistItemForm")
+ textarea.js-edit-checklist-item(rows='1' autofocus)
+ if $eq type 'item'
+ = item.title
+ else
+ = checklist.title
+ .edit-controls.clearfix
+ button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+ span(title=createdAt) {{ moment createdAt }}
+ if currentUser.isBoardMember
+ a.js-delete-checklist-item {{_ "delete"}}...
+
+template(name="checklistItems")
+ .checklist-items
+ each item in checklist.items
+ +inlinedForm(classNames="js-edit-checklist-item")
+ +editChecklistItemForm(type = 'item' item = item checklist = checklist)
+ else
+ +itemDetail(item = item checklist = checklist)
+ if currentUser.isBoardMember
+ +inlinedForm(classNames="js-add-checklist-item" checklist = checklist)
+ +addChecklistItemForm
+ else
+ a.add-checklist-item.js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ 'add-checklist-item'}}...
+
+template(name='itemDetail')
+ .item
+ .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}}") {{item.title}}
diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js
new file mode 100644
index 00000000..b8113a54
--- /dev/null
+++ b/client/components/cards/checklists.js
@@ -0,0 +1,74 @@
+BlazeComponent.extendComponent({
+ addChecklist(event) {
+ event.preventDefault();
+ const textarea = this.find('textarea.js-add-checklist-item');
+ const title = textarea.value.trim();
+ const cardId = this.currentData().cardId;
+ Checklists.insert({
+ cardId,
+ title,
+ });
+ },
+
+ addChecklistItem(event) {
+ event.preventDefault();
+ const textarea = this.find('textarea.js-add-checklist-item');
+ const title = textarea.value.trim();
+ const checklist = this.currentData().checklist;
+ checklist.addItem(title);
+ },
+
+ editChecklist(event) {
+ event.preventDefault();
+ const textarea = this.find('textarea.js-edit-checklist-item');
+ const title = textarea.value.trim();
+ const checklist = this.currentData().checklist;
+ checklist.setTitle(title);
+ },
+
+ editChecklistItem(event) {
+ event.preventDefault();
+
+ 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);
+ },
+
+ deleteItem() {
+ const checklist = this.currentData().checklist;
+ const item = this.currentData().item;
+ if (checklist && item && item._id) {
+ checklist.removeItem(item._id);
+ }
+ },
+
+ deleteChecklist() {
+ const checklist = this.currentData().checklist;
+ if (checklist && checklist._id) {
+ Checklists.remove(checklist._id);
+ }
+ },
+
+ pressKey(event) {
+ //If user press enter key inside a form, submit it, so user doesn't have to leave keyboard to submit a form.
+ if (event.keyCode === 13) {
+ event.preventDefault();
+ const $form = $(event.currentTarget).closest('form');
+ $form.find('button[type=submit]').click();
+ }
+ },
+
+ events() {
+ return [{
+ 'submit .js-add-checklist': this.addChecklist,
+ 'submit .js-edit-checklist-title': this.editChecklist,
+ 'submit .js-add-checklist-item': this.addChecklistItem,
+ 'submit .js-edit-checklist-item': this.editChecklistItem,
+ 'click .js-delete-checklist-item': this.deleteItem,
+ 'click .js-delete-checklist': this.deleteChecklist,
+ keydown: this.pressKey,
+ }];
+ },
+}).register('checklists');
diff --git a/client/components/cards/checklists.styl b/client/components/cards/checklists.styl
new file mode 100644
index 00000000..885d7528
--- /dev/null
+++ b/client/components/cards/checklists.styl
@@ -0,0 +1,68 @@
+.js-add-checklist
+ color: #8c8c8c
+
+textarea.js-add-checklist-item, textarea.js-edit-checklist-item
+ overflow: hidden
+ word-wrap: break-word
+ resize: none
+ height: 34px
+
+.delete-text
+ color: #8c8c8c
+ text-decoration: underline
+ word-wrap: break-word
+ float: right
+ padding-top: 6px
+ &:hover
+ color: inherit
+
+.checklist-title
+ .checkbox
+ float: left
+ width: 30px
+ height 30px
+ font-size: 18px
+ line-height: 30px
+
+ .title
+ font-size: 18px
+ line-height: 30px
+
+ .checklist-stat
+ margin: 0 0.5em
+ float: right
+ padding-top: 6px
+ &.is-finished
+ color: #3cb500
+
+ .js-delete-checklist
+ @extends .delete-text
+
+.checklist-items
+ margin: 0 0 0.5em 1.33em
+
+ .item
+ line-height: 25px
+ font-size: 1.1em
+ margin-top: 3px
+ display: flex
+
+ .check-box
+ margin-top: 5px
+ &.is-checked
+ border-bottom: 2px solid #3cb500
+ border-right: 2px solid #3cb500
+
+ .item-title
+ padding-left: 10px;
+ &.is-checked
+ color: #8c8c8c
+ font-style: italic
+
+ .js-delete-checklist-item
+ @extends .delete-text
+ padding: 12px 0 0 0
+
+ .add-checklist-item
+ padding-top: 0.5em
+ display: inline-block
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index edc7d2d3..8b46ee74 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -14,7 +14,7 @@ template(name="minicard")
.badges
if comments.count
.badge(title="{{_ 'card-comments-title' comments.count }}")
- span.badge-icon.fa.fa-comment-o
+ span.badge-icon.fa.fa-comment-o.badge-comment
span.badge-text= comments.count
if description
.badge.badge-state-image-only(title=description)
@@ -29,3 +29,8 @@ template(name="minicard")
if dueAt
.badge
+minicardDueDate
+ if checklists.count
+ .badge(class="{{#if checklistFinished}}is-finished{{/if}}")
+ span.badge-icon.fa.fa-check-square-o
+ span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
+
diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl
index a61f6067..12a89785 100644
--- a/client/components/cards/minicard.styl
+++ b/client/components/cards/minicard.styl
@@ -99,10 +99,26 @@
.badge-text
vertical-align: middle
+ &.is-finished
+ background: #3cb500
+ padding: 0px 3px
+ border-radius: 3px
+ color: white
+
+ .badge-icon,
+ .badge-text
+ vertical-align: middle//didn't figure why use top, it'd be easier to fill bg if it's middle. This was introduced in commit "91cfcf7b12b5e7c137c2e765b2c378dde6b82966" & "* Improve the design of the minicards badges" was mentioned.
+ &.badge-comment
+ margin-bottom: 0.1rem
+
.badge-text
font-size: 0.9em
padding-left: 2px
line-height: 14px
+ .check-list-text
+ padding-left: 0px
+ line-height: 12px
+
.minicard-members
float: right
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index 91c8e0af..1d803ee3 100644
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -36,10 +36,13 @@
"activity-removed": "removed %s from %s",
"activity-sent": "sent %s to %s",
"activity-unjoined": "unjoined %s",
+ "activity-checklist-added": "added checklist to %s",
"add": "Add",
"add-attachment": "Add an attachment",
"add-board": "Add a new board",
"add-card": "Add a card",
+ "add-checklist": "Add a checklist",
+ "add-checklist-item": "Add an item to checklist",
"add-cover": "Add Cover",
"add-label": "Add the label",
"add-list": "Add a list",
@@ -115,6 +118,7 @@
"changePasswordPopup-title": "Change Password",
"changePermissionsPopup-title": "Change Permissions",
"changeSettingsPopup-title": "Change Settings",
+ "checklists": "Checklists",
"click-to-star": "Click to star this board.",
"click-to-unstar": "Click to unstar this board.",
"clipboard" : "Clipboard or drag & drop",
diff --git a/models/activities.js b/models/activities.js
index aa2ea3ec..7d262ec6 100644
--- a/models/activities.js
+++ b/models/activities.js
@@ -35,6 +35,9 @@ Activities.helpers({
attachment() {
return Attachments.findOne(this.attachmentId);
},
+ checklist() {
+ return Checklists.findOne(this.checklistId);
+ },
});
Activities.before.insert((userId, doc) => {
@@ -102,6 +105,10 @@ if (Meteor.isServer) {
const attachment = activity.attachment();
params.attachment = attachment._id;
}
+ if (activity.checklistId) {
+ const checklist = activity.checklist();
+ params.checklist = checklist.title;
+ }
if (board) {
const watchingUsers = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId');
const trackingUsers = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId');
diff --git a/models/cards.js b/models/cards.js
index 9e7d58c8..f6bd0b06 100644
--- a/models/cards.js
+++ b/models/cards.js
@@ -141,6 +141,36 @@ Cards.helpers({
return cover && cover.url() && cover;
},
+ checklists() {
+ return Checklists.find({ cardId: this._id }, { sort: { createdAt: 1 }});
+ },
+
+ checklistItemCount() {
+ const checklists = this.checklists().fetch();
+ return checklists.map((checklist) => {
+ return checklist.itemCount();
+ }).reduce((prev, next) => {
+ return prev + next;
+ }, 0);
+ },
+
+ checklistFinishedCount() {
+ const checklists = this.checklists().fetch();
+ return checklists.map((checklist) => {
+ return checklist.finishedCount();
+ }).reduce((prev, next) => {
+ return prev + next;
+ }, 0);
+ },
+
+ checklistFinished() {
+ return this.hasChecklist() && this.checklistItemCount() === this.checklistFinishedCount();
+ },
+
+ hasChecklist() {
+ return this.checklistItemCount() !== 0;
+ },
+
absoluteUrl() {
const board = this.board();
return FlowRouter.url('card', {
diff --git a/models/checklists.js b/models/checklists.js
new file mode 100644
index 00000000..35be4dcc
--- /dev/null
+++ b/models/checklists.js
@@ -0,0 +1,164 @@
+Checklists = new Mongo.Collection('checklists');
+
+Checklists.attachSchema(new SimpleSchema({
+ cardId: {
+ type: String,
+ },
+ title: {
+ type: String,
+ },
+ items: {
+ type: [Object],
+ defaultValue: [],
+ },
+ 'items.$._id': {
+ type: String,
+ },
+ 'items.$.title': {
+ type: String,
+ },
+ 'items.$.isFinished': {
+ type: Boolean,
+ defaultValue: false,
+ },
+ finishedAt: {
+ type: Date,
+ optional: true,
+ },
+ createdAt: {
+ type: Date,
+ denyUpdate: false,
+ },
+}));
+
+Checklists.helpers({
+ itemCount () {
+ return this.items.length;
+ },
+ finishedCount () {
+ return this.items.filter((item) => {
+ return item.isFinished;
+ }).length;
+ },
+ isFinished () {
+ return 0 !== this.itemCount() && this.itemCount() === this.finishedCount();
+ },
+ getItem (_id) {
+ return _.findWhere(this.items, { _id });
+ },
+ itemIndex(itemId) {
+ return _.pluck(this.items, '_id').indexOf(itemId);
+ },
+});
+
+Checklists.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'],
+});
+
+Checklists.before.insert((userId, doc) => {
+ doc.createdAt = new Date();
+ if (!doc.userId) {
+ doc.userId = userId;
+ }
+});
+
+Checklists.mutations({
+ //for checklist itself
+ setTitle(title){
+ return { $set: { title }};
+ },
+ //for items in checklist
+ addItem(title) {
+ const itemCount = this.itemCount();
+ const _id = `${this._id}${itemCount}`;
+ return { $addToSet: {items: {_id, title, isFinished: false}} };
+ },
+ removeItem(itemId) {
+ 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 {};
+ },
+});
+
+if (Meteor.isServer) {
+ Checklists.after.insert((userId, doc) => {
+ Activities.insert({
+ userId,
+ activityType: 'addChecklist',
+ cardId: doc.cardId,
+ boardId: Cards.findOne(doc.cardId).boardId,
+ checklistId: doc._id,
+ });
+ });
+
+ //TODO: so there will be no activity for adding item into checklist, maybe will be implemented in the future.
+ // Checklists.after.update((userId, doc) => {
+ // console.log('update:', doc)
+ // Activities.insert({
+ // userId,
+ // activityType: 'addChecklist',
+ // boardId: doc.boardId,
+ // cardId: doc.cardId,
+ // checklistId: doc._id,
+ // });
+ // });
+
+ Checklists.before.remove((userId, doc) => {
+ const activity = Activities.findOne({ checklistId: doc._id });
+ if (activity) {
+ Activities.remove(activity._id);
+ }
+ });
+}
diff --git a/server/lib/utils.js b/server/lib/utils.js
index b59671fb..bc3807bb 100644
--- a/server/lib/utils.js
+++ b/server/lib/utils.js
@@ -5,3 +5,8 @@ allowIsBoardAdmin = function(userId, board) {
allowIsBoardMember = function(userId, board) {
return board && board.hasMember(userId);
};
+
+allowIsBoardMemberByCard = function(userId, card) {
+ const board = card.board();
+ return board && board.hasMember(userId);
+};
diff --git a/server/publications/boards.js b/server/publications/boards.js
index 89681978..133082dd 100644
--- a/server/publications/boards.js
+++ b/server/publications/boards.js
@@ -98,6 +98,7 @@ Meteor.publishRelations('board', function(boardId) {
this.cursor(Cards.find({ boardId }), function(cardId) {
this.cursor(CardComments.find({ cardId }));
this.cursor(Attachments.find({ cardId }));
+ this.cursor(Checklists.find({ cardId }));
});
if (board.members) {