Add checklist feature

This commit is contained in:
lksime 2017-01-20 21:05:48 +08:00
parent 38c143b8bf
commit 59731af139
15 changed files with 462 additions and 2 deletions

View file

@ -117,6 +117,8 @@
"Notifications": true,
"allowIsBoardAdmin": true,
"allowIsBoardMember": true,
"Emoji": true
"allowIsBoardMemberByCard": true,
"Emoji": true,
"Checklists": true
}
}

View file

@ -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')

View file

@ -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%)

View file

@ -72,6 +72,10 @@ template(name="cardDetails")
h3.card-details-item-title {{_ 'description'}}
+viewer
= description
hr
+checklists(cardId = _id)
if attachments.count
hr
h2

View file

@ -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}}

View file

@ -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');

View file

@ -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

View file

@ -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}}

View file

@ -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

View file

@ -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",

View file

@ -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');

View file

@ -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', {

164
models/checklists.js Normal file
View file

@ -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);
}
});
}

View file

@ -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);
};

View file

@ -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) {